feat(orchestrator): modèle de désignation d'orchestrateur + sink de diagnostic
Introduit le modèle AgentManifest { version, entries, orchestrator } et la
garde d'écriture directe may_write_directly(..., &OrchestratorDesignation) :
seul l'orchestrateur désigné peut écrire directement, les autres passent par
le rendez-vous médié. Câble la désignation à travers domain → application →
infrastructure → app-tauri (context_guard, service, lifecycle, ports).
Ajoute crates/application/src/diag.rs : sink de diagnostic best-effort, sans
dépendance, qui miroite les traces du rendez-vous inter-agents de
l'orchestrateur vers un fichier de log persistant (utile au lancement via
AppImage où stderr est jeté), avec la même discipline « zéro dépendance,
ne casse jamais le rendez-vous ».
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -122,6 +122,8 @@ pub struct CodexExecSession {
|
||||
command: String,
|
||||
/// Répertoire de travail (run dir isolé §14.1).
|
||||
cwd: String,
|
||||
/// Project/workspace roots that must be writable in Codex's CLI sandbox.
|
||||
writable_roots: Vec<String>,
|
||||
/// Id de conversation **du moteur** Codex, capté au premier tour, `None` avant.
|
||||
conversation_id: Mutex<Option<String>>,
|
||||
/// Plan de sandbox OS **par lancement** (lot LP4-4), porté dans chaque
|
||||
@ -143,6 +145,7 @@ impl CodexExecSession {
|
||||
command: impl Into<String>,
|
||||
cwd: impl Into<String>,
|
||||
seed_conversation_id: Option<String>,
|
||||
writable_roots: Vec<String>,
|
||||
sandbox: Option<SandboxPlan>,
|
||||
sandbox_enforcer: Option<Arc<dyn SandboxEnforcer>>,
|
||||
) -> Self {
|
||||
@ -150,6 +153,7 @@ impl CodexExecSession {
|
||||
id,
|
||||
command: command.into(),
|
||||
cwd: cwd.into(),
|
||||
writable_roots,
|
||||
conversation_id: Mutex::new(seed_conversation_id),
|
||||
sandbox,
|
||||
sandbox_enforcer,
|
||||
@ -160,18 +164,18 @@ impl CodexExecSession {
|
||||
///
|
||||
/// Format RÉEL vérifié 2026-06-10 (codex 0.137.0) :
|
||||
/// - Conversation neuve : `codex exec --json --skip-git-repo-check
|
||||
/// --sandbox workspace-write <prompt>`.
|
||||
/// --sandbox workspace-write --add-dir <project-root> <prompt>`.
|
||||
/// - Reprise (id connu) : `codex exec resume <thread_id> --json
|
||||
/// --skip-git-repo-check --sandbox workspace-write <prompt>`.
|
||||
/// --skip-git-repo-check --sandbox workspace-write --add-dir <project-root>
|
||||
/// <prompt>`.
|
||||
///
|
||||
/// **Autonomie d'écriture (D3)** : `--sandbox workspace-write` autorise l'agent à
|
||||
/// écrire dans son workspace. `codex exec` est déjà non-interactif (aucun prompt
|
||||
/// d'approbation possible), donc on ne passe **pas** `--ask-for-approval` : ce flag
|
||||
/// appartient à la commande interactive `codex`, pas à la sous-commande `exec` qui
|
||||
/// sort sur `error: unexpected argument '--ask-for-approval' found`. Défaut
|
||||
/// raisonnable, aligné sur l'autonomie projet (CLAUDE.md §12) ; à terme **piloté par
|
||||
/// les permissions de l'agent** (`.ideai/permissions.json` + sandbox OS) — non
|
||||
/// implémenté ici.
|
||||
/// sort sur `error: unexpected argument '--ask-for-approval' found`. Comme l'agent
|
||||
/// tourne depuis son run dir isolé, `--add-dir` expose explicitement le project root
|
||||
/// à la sandbox Codex pour que les écritures Git touchent le vrai workspace.
|
||||
fn build_spawn_line(&self, prompt: &str) -> SpawnLine {
|
||||
let mut args = vec!["exec".to_owned()];
|
||||
if let Some(id) = self.conversation_id.lock().expect("mutex sain").as_ref() {
|
||||
@ -182,6 +186,10 @@ impl CodexExecSession {
|
||||
args.push("--skip-git-repo-check".to_owned());
|
||||
args.push("--sandbox".to_owned());
|
||||
args.push("workspace-write".to_owned());
|
||||
for root in self.writable_roots.iter().filter(|root| !root.is_empty()) {
|
||||
args.push("--add-dir".to_owned());
|
||||
args.push(root.clone());
|
||||
}
|
||||
args.push(prompt.to_owned());
|
||||
SpawnLine {
|
||||
command: self.command.clone(),
|
||||
|
||||
@ -79,7 +79,7 @@ impl AgentSessionFactory for StructuredSessionFactory {
|
||||
async fn start(
|
||||
&self,
|
||||
profile: &AgentProfile,
|
||||
_ctx: &PreparedContext,
|
||||
ctx: &PreparedContext,
|
||||
cwd: &ProjectPath,
|
||||
session: &SessionPlan,
|
||||
sandbox: Option<&SandboxPlan>,
|
||||
@ -109,12 +109,18 @@ impl AgentSessionFactory for StructuredSessionFactory {
|
||||
// injection supplémentaire n'incombe ici en mode structuré (la CLI lit son
|
||||
// fichier conventionnel — CLAUDE.md / AGENTS.md — depuis le cwd).
|
||||
let session: Arc<dyn AgentSession> = match adapter {
|
||||
StructuredAdapter::Claude => {
|
||||
Arc::new(ClaudeSdkSession::new(id, command, cwd, seed, plan, enforcer))
|
||||
}
|
||||
StructuredAdapter::Codex => {
|
||||
Arc::new(CodexExecSession::new(id, command, cwd, seed, plan, enforcer))
|
||||
}
|
||||
StructuredAdapter::Claude => Arc::new(ClaudeSdkSession::new(
|
||||
id, command, cwd, seed, plan, enforcer,
|
||||
)),
|
||||
StructuredAdapter::Codex => Arc::new(CodexExecSession::new(
|
||||
id,
|
||||
command,
|
||||
cwd,
|
||||
seed,
|
||||
vec![ctx.project_root.clone()],
|
||||
plan,
|
||||
enforcer,
|
||||
)),
|
||||
};
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
@ -62,6 +62,7 @@ mod tests {
|
||||
PreparedContext {
|
||||
content: MarkdownDoc::new("# ctx"),
|
||||
relative_path: "CLAUDE.md".to_owned(),
|
||||
project_root: "/project".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,8 +241,7 @@ mod tests {
|
||||
// turn.started / turn.completed ⇒ heartbeat (vivacité non terminale, lot 1).
|
||||
let started = codex::parse_event(r#"{"type":"turn.started"}"#).expect("ok");
|
||||
assert_eq!(started.events, vec![ReplyEvent::Heartbeat]);
|
||||
let completed =
|
||||
codex::parse_event(r#"{"type":"turn.completed","usage":{}}"#).expect("ok");
|
||||
let completed = codex::parse_event(r#"{"type":"turn.completed","usage":{}}"#).expect("ok");
|
||||
assert_eq!(completed.events, vec![ReplyEvent::Heartbeat]);
|
||||
|
||||
let msg = codex::parse_event(
|
||||
@ -310,6 +310,7 @@ mod tests {
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
@ -321,7 +322,14 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn stream_is_closed_after_final() {
|
||||
let fake = FakeCli::printing(&claude_script());
|
||||
let session = ClaudeSdkSession::new(SessionId::new_random(), fake.command(), "/", None, None, None);
|
||||
let session = ClaudeSdkSession::new(
|
||||
SessionId::new_random(),
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let stream = session.send("x").await.expect("send ok");
|
||||
let events: Vec<_> = stream.collect();
|
||||
let after_final = events
|
||||
@ -339,7 +347,14 @@ mod tests {
|
||||
r#"{"type":"system","subtype":"init","session_id":"c"}"#,
|
||||
"{ ceci n'est pas du json",
|
||||
]);
|
||||
let session = ClaudeSdkSession::new(SessionId::new_random(), fake.command(), "/", None, None, None);
|
||||
let session = ClaudeSdkSession::new(
|
||||
SessionId::new_random(),
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
match session.send("x").await {
|
||||
Err(AgentSessionError::Decode(_)) => {}
|
||||
Err(other) => panic!("attendu Decode, vu: {other:?}"),
|
||||
@ -437,6 +452,42 @@ mod tests {
|
||||
assert_eq!(content_cx, "réponse Codex");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn factory_passes_project_root_to_codex_add_dir() {
|
||||
let factory = StructuredSessionFactory::new();
|
||||
let (cmd, argv) = make_recording_fake(&[
|
||||
r#"{"type":"thread.started","thread_id":"cx-new"}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"ok"}}"#,
|
||||
]);
|
||||
let codex = structured_profile(StructuredAdapter::Codex, &cmd);
|
||||
let ctx = PreparedContext {
|
||||
content: MarkdownDoc::new("# ctx"),
|
||||
relative_path: "AGENTS.md".to_owned(),
|
||||
project_root: "/project/root".to_owned(),
|
||||
};
|
||||
|
||||
let session = factory
|
||||
.start(&codex, &ctx, &cwd(), &SessionPlan::None, None)
|
||||
.await
|
||||
.expect("start Codex ok");
|
||||
let content = drain_final(session.as_ref()).await;
|
||||
assert_eq!(content, "ok");
|
||||
|
||||
let recorded = std::fs::read_to_string(&argv).expect("argv");
|
||||
let args: Vec<&str> = recorded.lines().collect();
|
||||
assert!(
|
||||
args.windows(2).any(|w| w == ["--add-dir", "/project/root"]),
|
||||
"factory must relay PreparedContext.project_root to Codex --add-dir, got: {args:?}"
|
||||
);
|
||||
assert!(
|
||||
!args.contains(&"--ask-for-approval"),
|
||||
"codex exec must not receive unsupported approval flags, got: {args:?}"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(&cmd);
|
||||
let _ = std::fs::remove_file(&argv);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn factory_resume_seeds_conversation_id() {
|
||||
let factory = StructuredSessionFactory::new();
|
||||
@ -564,10 +615,14 @@ mod tests {
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
static C: AtomicU64 = AtomicU64::new(0);
|
||||
let n = C.fetch_add(1, Ordering::Relaxed);
|
||||
let mut bin = std::env::temp_dir();
|
||||
bin.push(format!("idea-rec-cli-{}-{n}", std::process::id()));
|
||||
let mut argv = std::env::temp_dir();
|
||||
argv.push(format!("idea-rec-argv-{}-{n}", std::process::id()));
|
||||
let dir = std::env::current_dir()
|
||||
.expect("cwd")
|
||||
.join("target")
|
||||
.join("test-fakes")
|
||||
.join("session");
|
||||
std::fs::create_dir_all(&dir).expect("create rec fake dir");
|
||||
let bin = dir.join(format!("idea-rec-cli-{}-{n}", std::process::id()));
|
||||
let argv = dir.join(format!("idea-rec-argv-{}-{n}", std::process::id()));
|
||||
|
||||
let mut s = String::from("#!/bin/sh\n");
|
||||
// Enregistre chaque argument sur sa propre ligne dans le sidecar.
|
||||
@ -775,7 +830,15 @@ mod tests {
|
||||
r#"{"type":"item.completed","item":{"id":"i1","type":"agent_message","text":"fin"}}"#,
|
||||
r#"{"type":"turn.completed","usage":{}}"#,
|
||||
]);
|
||||
let s = CodexExecSession::new(SessionId::new_random(), fake.command(), "/", None, None, None);
|
||||
let s = CodexExecSession::new(
|
||||
SessionId::new_random(),
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let events: Vec<_> = s.send("x").await.expect("send").collect();
|
||||
let finals = events
|
||||
.iter()
|
||||
@ -789,7 +852,10 @@ mod tests {
|
||||
.skip(1)
|
||||
.filter(|e| matches!(e, ReplyEvent::Final { .. }))
|
||||
.count();
|
||||
assert_eq!(after_final_terminals, 0, "aucun second Final après le premier");
|
||||
assert_eq!(
|
||||
after_final_terminals, 0,
|
||||
"aucun second Final après le premier"
|
||||
);
|
||||
}
|
||||
|
||||
/// LIMITE/ÉCART (à arbitrer) : un flux SANS `Final` ne provoque PAS d'erreur au
|
||||
@ -803,7 +869,14 @@ mod tests {
|
||||
r#"{"type":"system","subtype":"init","session_id":"c"}"#,
|
||||
r#"{"type":"assistant","message":{"content":[{"type":"text","text":"a"}]}}"#,
|
||||
]);
|
||||
let s = ClaudeSdkSession::new(SessionId::new_random(), fake.command(), "/", None, None, None);
|
||||
let s = ClaudeSdkSession::new(
|
||||
SessionId::new_random(),
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let events: Vec<_> = s.send("x").await.expect("send ok").collect();
|
||||
let finals = events
|
||||
.iter()
|
||||
@ -959,6 +1032,7 @@ mod tests {
|
||||
cmd.clone(),
|
||||
"/",
|
||||
Some("cx-id".to_owned()),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
@ -983,7 +1057,8 @@ mod tests {
|
||||
r#"{"type":"system","subtype":"init","session_id":"captured-1"}"#,
|
||||
r#"{"type":"result","subtype":"success","result":"r","session_id":"captured-1"}"#,
|
||||
]);
|
||||
let session = ClaudeSdkSession::new(SessionId::new_random(), cmd.clone(), "/", None, None, None);
|
||||
let session =
|
||||
ClaudeSdkSession::new(SessionId::new_random(), cmd.clone(), "/", None, None, None);
|
||||
assert_eq!(session.conversation_id(), None);
|
||||
let _ = session.send("t1").await.expect("t1");
|
||||
assert_eq!(session.conversation_id().as_deref(), Some("captured-1"));
|
||||
@ -1093,6 +1168,7 @@ mod tests {
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
@ -1123,7 +1199,14 @@ mod tests {
|
||||
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"a"},{"type":"tool_use","name":"T"},{"type":"text","text":"b"}]},"session_id":"flow-1","parent_tool_use_id":null}"#,
|
||||
r#"{"type":"result","subtype":"success","is_error":false,"result":"final-ok","session_id":"flow-1","num_turns":1}"#,
|
||||
]);
|
||||
let session = ClaudeSdkSession::new(SessionId::new_random(), fake.command(), "/", None, None, None);
|
||||
let session = ClaudeSdkSession::new(
|
||||
SessionId::new_random(),
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let events: Vec<ReplyEvent> = session.send("x").await.expect("send ok").collect();
|
||||
assert_eq!(
|
||||
events,
|
||||
@ -1160,7 +1243,8 @@ mod tests {
|
||||
r#"{"type":"system","subtype":"init","session_id":"new-1"}"#,
|
||||
r#"{"type":"result","subtype":"success","result":"r","session_id":"new-1"}"#,
|
||||
]);
|
||||
let session = ClaudeSdkSession::new(SessionId::new_random(), cmd.clone(), "/", None, None, None);
|
||||
let session =
|
||||
ClaudeSdkSession::new(SessionId::new_random(), cmd.clone(), "/", None, None, None);
|
||||
let _ = session.send("bonjour").await.expect("send ok");
|
||||
let recorded = std::fs::read_to_string(&argv).expect("argv");
|
||||
let args: Vec<&str> = recorded.lines().collect();
|
||||
@ -1192,7 +1276,15 @@ mod tests {
|
||||
r#"{"type":"thread.started","thread_id":"cx-new"}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"ok"}}"#,
|
||||
]);
|
||||
let session = CodexExecSession::new(SessionId::new_random(), cmd.clone(), "/", None, None, None);
|
||||
let session = CodexExecSession::new(
|
||||
SessionId::new_random(),
|
||||
cmd.clone(),
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let _ = session.send("salut").await.expect("send ok");
|
||||
let recorded = std::fs::read_to_string(&argv).expect("argv");
|
||||
let args: Vec<&str> = recorded.lines().collect();
|
||||
@ -1217,7 +1309,8 @@ mod tests {
|
||||
// =====================================================================
|
||||
// DURCISSEMENT QA (lot D3, §17.9 D3 — fix codex 0.137) — autonomie
|
||||
// d'écriture Codex : la commande générée porte EXACTEMENT
|
||||
// [exec, --json, --skip-git-repo-check, --sandbox, workspace-write, <prompt>]
|
||||
// [exec, --json, --skip-git-repo-check, --sandbox, workspace-write,
|
||||
// --add-dir, <project-root>, <prompt>]
|
||||
// (resume <id> en tête pour une reprise). Le flag `--ask-for-approval never`
|
||||
// a été RETIRÉ : `codex exec` 0.137 ne le connaît pas (`error: unexpected
|
||||
// argument`) et est déjà non-interactif. Ce test verrouille l'argv exact pour
|
||||
@ -1226,15 +1319,23 @@ mod tests {
|
||||
// =====================================================================
|
||||
|
||||
/// Conversation NEUVE : argv EXACT `[exec, --json, --skip-git-repo-check,
|
||||
/// --sandbox, workspace-write, <prompt>]`. Pas de sous-commande `resume`,
|
||||
/// pas de `--ask-for-approval`.
|
||||
/// --sandbox, workspace-write, --add-dir, <project-root>, <prompt>]`.
|
||||
/// Pas de sous-commande `resume`, pas de `--ask-for-approval`.
|
||||
#[tokio::test]
|
||||
async fn codex_new_conversation_command_carries_exact_args() {
|
||||
let (cmd, argv) = make_recording_fake(&[
|
||||
r#"{"type":"thread.started","thread_id":"cx-new"}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"ok"}}"#,
|
||||
]);
|
||||
let session = CodexExecSession::new(SessionId::new_random(), cmd.clone(), "/", None, None, None);
|
||||
let session = CodexExecSession::new(
|
||||
SessionId::new_random(),
|
||||
cmd.clone(),
|
||||
"/",
|
||||
None,
|
||||
vec!["/project/root".to_owned()],
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let _ = session.send("salut").await.expect("send ok");
|
||||
let recorded = std::fs::read_to_string(&argv).expect("argv");
|
||||
let args: Vec<&str> = recorded.lines().collect();
|
||||
@ -1247,6 +1348,8 @@ mod tests {
|
||||
"--skip-git-repo-check",
|
||||
"--sandbox",
|
||||
"workspace-write",
|
||||
"--add-dir",
|
||||
"/project/root",
|
||||
"salut",
|
||||
],
|
||||
"argv neuf doit être exact (sans resume, sans --ask-for-approval), vu: {args:?}"
|
||||
@ -1256,8 +1359,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// REPRISE (seed d'id) : argv EXACT `[exec, resume, <id>, --json,
|
||||
/// --skip-git-repo-check, --sandbox, workspace-write, <prompt>]`. Toujours
|
||||
/// pas de `--ask-for-approval`.
|
||||
/// --skip-git-repo-check, --sandbox, workspace-write, --add-dir, <project-root>,
|
||||
/// <prompt>]`. Toujours pas de `--ask-for-approval`.
|
||||
#[tokio::test]
|
||||
async fn codex_resume_command_carries_exact_args() {
|
||||
let (cmd, argv) = make_recording_fake(&[
|
||||
@ -1268,6 +1371,7 @@ mod tests {
|
||||
cmd.clone(),
|
||||
"/",
|
||||
Some("cx-id".to_owned()),
|
||||
vec!["/project/root".to_owned()],
|
||||
None,
|
||||
None,
|
||||
);
|
||||
@ -1285,6 +1389,8 @@ mod tests {
|
||||
"--skip-git-repo-check",
|
||||
"--sandbox",
|
||||
"workspace-write",
|
||||
"--add-dir",
|
||||
"/project/root",
|
||||
"vas-y",
|
||||
],
|
||||
"argv reprise doit être exact (resume <id> en tête, sans --ask-for-approval), vu: {args:?}"
|
||||
@ -1339,13 +1445,19 @@ mod tests {
|
||||
#[test]
|
||||
fn parse_reset_ms_integer_seconds_are_scaled_to_ms() {
|
||||
// < 10^12 ⇒ secondes ⇒ ×1000.
|
||||
assert_eq!(parse_reset_ms(&json!({ "reset": 1_700_000_000_i64 })), Some(1_700_000_000_000));
|
||||
assert_eq!(
|
||||
parse_reset_ms(&json!({ "reset": 1_700_000_000_i64 })),
|
||||
Some(1_700_000_000_000)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_reset_ms_integer_millis_are_kept_as_is() {
|
||||
// ≥ 10^12 ⇒ déjà des millisecondes ⇒ tel quel.
|
||||
assert_eq!(parse_reset_ms(&json!({ "reset": 1_700_000_000_000_i64 })), Some(1_700_000_000_000));
|
||||
assert_eq!(
|
||||
parse_reset_ms(&json!({ "reset": 1_700_000_000_000_i64 })),
|
||||
Some(1_700_000_000_000)
|
||||
);
|
||||
}
|
||||
|
||||
/// Le SEUIL exact (10^12) : juste en-dessous ⇒ secondes (×1000) ; pile/au-dessus
|
||||
@ -1388,12 +1500,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_reset_ms_string_integer_uses_seconds_heuristic() {
|
||||
assert_eq!(parse_reset_ms(&json!({ "reset": "1700000000" })), Some(1_700_000_000_000));
|
||||
assert_eq!(
|
||||
parse_reset_ms(&json!({ "reset": "1700000000" })),
|
||||
Some(1_700_000_000_000)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_reset_ms_string_float_uses_seconds_heuristic() {
|
||||
assert_eq!(parse_reset_ms(&json!({ "reset": "1700000000.5" })), Some(1_700_000_000_500));
|
||||
assert_eq!(
|
||||
parse_reset_ms(&json!({ "reset": "1700000000.5" })),
|
||||
Some(1_700_000_000_500)
|
||||
);
|
||||
}
|
||||
|
||||
// ---- parse_reset_ms : ISO-8601 / RFC3339 (parseur maison) --------------
|
||||
@ -1487,11 +1605,20 @@ mod tests {
|
||||
// Pas de séparateur de date/heure.
|
||||
assert_eq!(parse_reset_ms(&json!({ "reset": "2023-11-14" })), None);
|
||||
// Année non numérique.
|
||||
assert_eq!(parse_reset_ms(&json!({ "reset": "abcd-11-14T00:00:00Z" })), None);
|
||||
assert_eq!(
|
||||
parse_reset_ms(&json!({ "reset": "abcd-11-14T00:00:00Z" })),
|
||||
None
|
||||
);
|
||||
// Composante de date manquante (pas de jour).
|
||||
assert_eq!(parse_reset_ms(&json!({ "reset": "2023-11T00:00:00Z" })), None);
|
||||
assert_eq!(
|
||||
parse_reset_ms(&json!({ "reset": "2023-11T00:00:00Z" })),
|
||||
None
|
||||
);
|
||||
// Trop de composantes de date.
|
||||
assert_eq!(parse_reset_ms(&json!({ "reset": "2023-11-14-9T00:00:00Z" })), None);
|
||||
assert_eq!(
|
||||
parse_reset_ms(&json!({ "reset": "2023-11-14-9T00:00:00Z" })),
|
||||
None
|
||||
);
|
||||
// Minute manquante dans l'heure.
|
||||
assert_eq!(parse_reset_ms(&json!({ "reset": "2023-11-14T22Z" })), None);
|
||||
}
|
||||
@ -1614,7 +1741,14 @@ mod tests {
|
||||
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"ap"}]},"session_id":"rl-1","parent_tool_use_id":null}"#,
|
||||
r#"{"type":"result","subtype":"success","is_error":false,"result":"ok","session_id":"rl-1","num_turns":1}"#,
|
||||
]);
|
||||
let session = ClaudeSdkSession::new(SessionId::new_random(), fake.command(), "/", None, None, None);
|
||||
let session = ClaudeSdkSession::new(
|
||||
SessionId::new_random(),
|
||||
fake.command(),
|
||||
"/",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let events: Vec<ReplyEvent> = session.send("x").await.expect("send ok").collect();
|
||||
assert_eq!(
|
||||
events,
|
||||
|
||||
@ -158,8 +158,7 @@ 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);
|
||||
// Le récepteur peut avoir abandonné (timeout) : on ignore l'erreur d'envoi.
|
||||
let _ = done_tx.send(result);
|
||||
});
|
||||
|
||||
@ -187,7 +187,10 @@ async fn structured_run_turn_without_plan_does_not_sandbox() {
|
||||
.expect("run_turn natif réussit");
|
||||
|
||||
assert_eq!(lines, vec![RESULT_LINE.to_owned()]);
|
||||
assert!(allowed_marker.exists(), "écriture in-grant réussit (sans plan)");
|
||||
assert!(
|
||||
allowed_marker.exists(),
|
||||
"écriture in-grant réussit (sans plan)"
|
||||
);
|
||||
assert!(
|
||||
denied_marker.exists(),
|
||||
"sans plan, l'écriture hors-grant DOIT réussir: {denied_marker:?} absent — restriction ambiante anormale"
|
||||
@ -278,7 +281,10 @@ async fn structured_run_turn_fail_closed_no_child_on_enforce_err() {
|
||||
async fn structured_run_turn_none_plan_is_native_path() {
|
||||
let dir = fresh_dir("p4");
|
||||
let marker = dir.join("native.txt");
|
||||
let script = format!("printf '%s\\n' '{RESULT_LINE}'; echo ok > '{}'", marker.display());
|
||||
let script = format!(
|
||||
"printf '%s\\n' '{RESULT_LINE}'; echo ok > '{}'",
|
||||
marker.display()
|
||||
);
|
||||
let spec = SpawnLine {
|
||||
command: "sh".to_owned(),
|
||||
args: vec!["-c".to_owned(), script],
|
||||
@ -327,7 +333,10 @@ async fn structured_two_turns_disjoint_grants_are_confined() {
|
||||
run_turn(&spec_a, None, Some(&enforcer))
|
||||
.await
|
||||
.expect("tour A ok");
|
||||
assert!(a_in.exists(), "tour A: écriture dans son grant (A) doit réussir");
|
||||
assert!(
|
||||
a_in.exists(),
|
||||
"tour A: écriture dans son grant (A) doit réussir"
|
||||
);
|
||||
assert!(
|
||||
!a_into_b.exists(),
|
||||
"tour A: écriture dans B (hors grant A) doit être bloquée"
|
||||
@ -419,7 +428,9 @@ async fn structured_sandboxed_turn_preserves_conversation_id() {
|
||||
use super::factory::StructuredSessionFactory;
|
||||
|
||||
if !landlock_is_enforced() {
|
||||
eprintln!("skip structured_sandboxed_turn_preserves_conversation_id: Landlock indisponible");
|
||||
eprintln!(
|
||||
"skip structured_sandboxed_turn_preserves_conversation_id: Landlock indisponible"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -448,6 +459,7 @@ async fn structured_sandboxed_turn_preserves_conversation_id() {
|
||||
let ctx = PreparedContext {
|
||||
content: MarkdownDoc::new("# ctx"),
|
||||
relative_path: "CLAUDE.md".to_owned(),
|
||||
project_root: run_dir.to_string_lossy().into_owned(),
|
||||
};
|
||||
let cwd = ProjectPath::new(run_dir.to_string_lossy().into_owned()).expect("cwd absolu");
|
||||
let plan = rw_plan(&run_dir); // plan write-only ⇒ reads/exec du fake non gênés
|
||||
@ -466,7 +478,10 @@ async fn structured_sandboxed_turn_preserves_conversation_id() {
|
||||
.iter()
|
||||
.filter(|e| matches!(e, ReplyEvent::Final { .. }))
|
||||
.count();
|
||||
assert_eq!(finals, 1, "un Final attendu malgré le sandbox, vu: {events:?}");
|
||||
assert_eq!(
|
||||
finals, 1,
|
||||
"un Final attendu malgré le sandbox, vu: {events:?}"
|
||||
);
|
||||
|
||||
// LE POINT : l'id de conversation a bien été capté sous enforcement actif.
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user