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:
@ -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}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user