feat(diag): instrumente les blocages de délégation idea_ask_agent

Ajoute des logs diag! sans changement sémantique le long du canal de
délégation inter-agents (application/orchestrator, infrastructure
input, mailbox, mcp server & tools) pour diagnostiquer les blocages
récurrents de idea_ask_agent. Couvert par les tests existants étendus
(mcp_server, mailbox, input, tools, orchestrator_service).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 09:41:02 +02:00
parent 61c330a54e
commit e5e412bdf2
6 changed files with 426 additions and 43 deletions

View File

@ -1249,3 +1249,110 @@ async fn ask_agent_rendezvous_times_out_with_a_jsonrpc_error() {
);
assert!(response.result.is_none(), "error responses carry no result");
}
// ---------------------------------------------------------------------------
// Diagnostics capture (instrumentation des blocages de délégation)
//
// Best-effort diag sink (`application::diag`) mirrored to a temp file. The sink's
// path is a process-wide OnceLock, so every capture test in this binary points it
// at the SAME canonical file and asserts on lines carrying tokens unique to the
// test (a distinctive target name / agent id), immune to parallelism and re-runs.
// ---------------------------------------------------------------------------
/// Points the process-wide diag sink at one canonical temp file (idempotent) and
/// returns it.
fn diag_log_path() -> std::path::PathBuf {
let path = std::env::temp_dir().join("idea-diag-mcp-server-test.log");
application::diag::set_log_path(path.clone());
path
}
fn read_diag(path: &std::path::Path) -> String {
std::fs::read_to_string(path).unwrap_or_default()
}
/// Proves the `[mcp]` rendezvous trace: a wedged `idea_ask_agent` (target that
/// never replies) emits begin → armed → EXPIRED, plus the `[mcp] mapped` beacon.
#[tokio::test]
async fn diag_capture_mcp_ask_begin_armed_expired() {
let log = diag_log_path();
// Unique target name ⇒ only THIS test's beacons match `target=diag-wedge`.
let contexts = FakeContexts::new();
let agent_id = contexts.seed_agent("diag-wedge");
let (service, _mailbox, sessions) = build_service_with_mailbox(contexts);
seed_live_pty(
&sessions,
agent_id,
SessionId::from_uuid(Uuid::from_u128(0xD1A6_0911)),
);
let server = server(service).with_ask_rendezvous_timeout(std::time::Duration::from_millis(50));
let raw = tools_call(
1,
"idea_ask_agent",
json!({ "target": "diag-wedge", "task": "never answered diag" }),
);
let response =
tokio::time::timeout(std::time::Duration::from_secs(10), server.handle_raw(&raw))
.await
.expect("must not hang past the injected timeout")
.expect("reply owed");
assert!(
response.error.is_some(),
"wedged ask returns a JSON-RPC error"
);
let body = read_diag(&log);
assert!(
body.contains("[mcp] mapped kind=ask_agent target=diag-wedge"),
"mapped beacon missing: {body}"
);
assert!(
body.contains("[mcp] tools_call begin tool=idea_ask_agent")
&& body.contains("target=diag-wedge"),
"begin beacon missing: {body}"
);
assert!(
body.contains("[mcp] ask armed") && body.contains("target=diag-wedge"),
"armed beacon missing: {body}"
);
assert!(
body.contains("[mcp] ask EXPIRED") && body.contains("target=diag-wedge"),
"EXPIRED beacon missing: {body}"
);
}
/// Proves the `[rendezvous] idea_reply received` beacon fires BEFORE correlation and
/// is followed by `UNMATCHED` when no in-flight ask exists for the emitter.
#[tokio::test]
async fn diag_capture_idea_reply_received_then_unmatched() {
let log = diag_log_path();
let contexts = FakeContexts::new();
let (service, _mailbox, _sessions) = build_service_with_mailbox(contexts);
// A distinctive emitter id ⇒ match only this test's reply beacons in the log.
let from = AgentId::from_uuid(Uuid::from_u128(0xD1A6_0000_0000_0000_0000_0000_0000_0042));
let cmd = domain::OrchestratorCommand::Reply {
from,
ticket: None,
result: "orphan reply".to_owned(),
};
// No ask is in flight for `from` ⇒ dispatch returns an Invalid error (unmatched).
let outcome = service.dispatch(&project(), cmd).await;
assert!(outcome.is_err(), "an orphan idea_reply is a typed error");
let body = read_diag(&log);
let from = from.to_string();
assert!(
body.contains(&format!(
"[rendezvous] idea_reply received: from agent {from}"
)),
"received beacon missing: {body}"
);
assert!(
body.contains(&format!(
"[rendezvous] idea_reply UNMATCHED: from agent {from}"
)),
"UNMATCHED beacon missing: {body}"
);
}