chore(wip): checkpoint P8/C avant chantier Codex inter-agents

Sauvegarde de l'arbre de travail en cours (persistance P8, conversations
C-series, write-portal frontend, médiation d'entrée) avant d'attaquer le
support de la délégation inter-agents pour les profils Codex.

Le round-trip inter-agent question/réponse est couvert sans tokens par
les tests loopback existants (state::mcp_e2e_loopback_tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 21:42:53 +02:00
parent 4509f0db9d
commit fdcf16c387
76 changed files with 3783 additions and 1404 deletions

View File

@ -126,7 +126,10 @@ fn pump_delivers_deltas_then_exactly_one_final_in_order() {
"deltas in order, then the single Final"
);
// Exactly one Final, and it is last (deterministic terminal chunk).
let finals = got.iter().filter(|c| matches!(c, ReplyChunk::Final { .. })).count();
let finals = got
.iter()
.filter(|c| matches!(c, ReplyChunk::Final { .. }))
.count();
assert_eq!(finals, 1, "exactly one Final per turn");
assert!(matches!(got.last(), Some(ReplyChunk::Final { .. })));
}
@ -227,8 +230,12 @@ fn scrollback_accumulates_every_routed_chunk_in_order() {
gen,
vec![
ReplyEvent::TextDelta { text: "x".into() },
ReplyEvent::ToolActivity { label: "runs".into() },
ReplyEvent::Final { content: "x".into() },
ReplyEvent::ToolActivity {
label: "runs".into(),
},
ReplyEvent::Final {
content: "x".into(),
},
],
);
@ -236,7 +243,9 @@ fn scrollback_accumulates_every_routed_chunk_in_order() {
bridge.scrollback(&session),
vec![
delta("x"),
ReplyChunk::ToolActivity { label: "runs".into() },
ReplyChunk::ToolActivity {
label: "runs".into()
},
final_chunk("x"),
],
"scrollback retains the full conversation, in order"

View File

@ -148,7 +148,8 @@ fn orchestration_source_dto_serialises_lowercase() {
use domain::events::OrchestrationSource;
// File → "file"; Mcp → "mcp" (camelCase rename on a unit enum = lowercase).
let file = serde_json::to_value(OrchestrationSourceDto::from(OrchestrationSource::File)).unwrap();
let file =
serde_json::to_value(OrchestrationSourceDto::from(OrchestrationSource::File)).unwrap();
assert_eq!(file, json!("file"));
let mcp = serde_json::to_value(OrchestrationSourceDto::from(OrchestrationSource::Mcp)).unwrap();

View File

@ -8,9 +8,9 @@
use app_tauri_lib::dto::{CellKind, ReattachChatDto, ReplyChunk, TerminalSessionDto};
use application::{LaunchAgentOutput, StructuredSessionDescriptor};
use domain::{AgentId, NodeId, PtySize, SessionKind, SessionStatus, TerminalSession};
use domain::project::ProjectPath;
use domain::SessionId;
use domain::{AgentId, NodeId, PtySize, SessionKind, SessionStatus, TerminalSession};
use serde_json::json;
use uuid::Uuid;
@ -49,8 +49,12 @@ fn reply_chunk_final_serialises_exact_camel_case() {
fn reply_chunk_round_trips_through_json_for_every_variant() {
for chunk in [
ReplyChunk::TextDelta { text: "x".into() },
ReplyChunk::ToolActivity { label: "runs".into() },
ReplyChunk::Final { content: "y".into() },
ReplyChunk::ToolActivity {
label: "runs".into(),
},
ReplyChunk::Final {
content: "y".into(),
},
] {
let v = serde_json::to_value(&chunk).unwrap();
let back: ReplyChunk = serde_json::from_value(v).unwrap();
@ -84,7 +88,9 @@ fn reattach_chat_dto_serialises_camel_case_with_typed_scrollback() {
session_id: "sess-1".into(),
scrollback: vec![
ReplyChunk::TextDelta { text: "Hi".into() },
ReplyChunk::Final { content: "Hi".into() },
ReplyChunk::Final {
content: "Hi".into(),
},
],
};
let v = serde_json::to_value(&dto).unwrap();

View File

@ -16,9 +16,7 @@ use async_trait::async_trait;
use app_tauri_lib::dto::LiveAgentListDto;
use application::{LiveSessions, StructuredSessions, TerminalSessions};
use domain::ports::{AgentSession, AgentSessionError, PtyHandle, ReplyStream};
use domain::{
AgentId, NodeId, ProjectPath, PtySize, SessionId, SessionKind, TerminalSession,
};
use domain::{AgentId, NodeId, ProjectPath, PtySize, SessionId, SessionKind, TerminalSession};
use uuid::Uuid;
// --- petits constructeurs déterministes ------------------------------------
@ -160,7 +158,11 @@ fn same_agent_in_both_registries_is_deduplicated() {
structured.insert(fake(sid(2)), a, nid(200));
let dto = dto_for(&pty, &structured);
assert_eq!(dto.0.len(), 1, "un même agent ne doit apparaître qu'une fois");
assert_eq!(
dto.0.len(),
1,
"un même agent ne doit apparaître qu'une fois"
);
assert_eq!(dto.0[0].agent_id, a.to_string());
// On garde la première occurrence (PTY, listé en premier par l'agrégateur).
assert_eq!(dto.0[0].node_id, nid(100).to_string());

View File

@ -164,10 +164,7 @@ async fn stop_watch_unregisters_the_mcp_server() {
assert!(has_mcp(&state, &project.id));
state.stop_orchestrator_watch(&project.id);
assert!(
!has_mcp(&state, &project.id),
"MCP server removed on close"
);
assert!(!has_mcp(&state, &project.id), "MCP server removed on close");
assert_eq!(mcp_count(&state), 0);
// Stopping an unknown project is a no-op (does not panic) for the MCP twin too.
@ -274,7 +271,10 @@ async fn double_open_keeps_a_single_endpoint_no_address_in_use() {
// socket) — it returns early. One endpoint, still bound, no panic.
state.ensure_orchestrator_watch(&project);
assert_eq!(mcp_count(&state), 1, "one endpoint per project");
assert!(socket_exists(&project), "endpoint still bound after re-open");
assert!(
socket_exists(&project),
"endpoint still bound after re-open"
);
state.stop_orchestrator_watch(&project.id);
}