feat(domain,infra,app): borne summary_md du handoff + durcissement seam LLM (LS5)
Clôt le must-have perf du handoff : - domain : HANDOFF_SUMMARY_MAX_CHARS + bound_handoff_summary (helpers privés), re-export lib. - infrastructure : TURN_LINE_MAX_CHARS + élision dans render_turn (summarizer), re-exports. - application : borne entre fold et save (conversation/record), borne défensive dans resolve_handoff (agent/lifecycle) + module test. - seam LLM prêt mais NON activé (aucun LLM câblé). - tests (QA, verts) : conversation_log, conversation_record, agent_lifecycle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -2448,6 +2448,88 @@ async fn launch_with_store_without_handoff_omits_resume_section() {
|
||||
);
|
||||
}
|
||||
|
||||
/// LS5 — un `handoff.md` **legacy surdimensionné** (écrit avant LS5, non borné) chargé
|
||||
/// au lancement ⇒ la borne défensive de `resolve_handoff` ramène le `summary_md` injecté
|
||||
/// sous [`domain::HANDOFF_SUMMARY_MAX_CHARS`], sans migration de données ni crash. On
|
||||
/// vérifie sur la section `# Reprise de la conversation` réellement écrite dans le run dir.
|
||||
#[tokio::test]
|
||||
async fn launch_bounds_oversize_legacy_handoff_resume_section() {
|
||||
// Handoff legacy géant : 500 lignes-tours bien formées, objectif absent (⇒ la section
|
||||
// injectée = le seul summary, comparable directement au plafond).
|
||||
let mut summary = String::new();
|
||||
for i in 0..500u32 {
|
||||
if i > 0 {
|
||||
summary.push('\n');
|
||||
}
|
||||
summary.push_str(&format!(
|
||||
"- **Response:** tour numero {i} {}",
|
||||
"x".repeat(30)
|
||||
));
|
||||
}
|
||||
assert!(
|
||||
summary.chars().count() > domain::HANDOFF_SUMMARY_MAX_CHARS,
|
||||
"pré-condition : le handoff legacy dépasse le plafond ({})",
|
||||
summary.chars().count()
|
||||
);
|
||||
|
||||
let conv = conv_id(123);
|
||||
let store = FakeHandoffStore::with(conv, handoff_for(&summary, None));
|
||||
let provider = Arc::new(FakeHandoffProvider(Arc::new(store))) as Arc<dyn HandoffProvider>;
|
||||
let (launch, agent, fs) = launch_with_handoff(Some(provider));
|
||||
|
||||
let mut input = launch_input(agent.id);
|
||||
input.conversation_id = Some(Uuid::from_u128(123).to_string());
|
||||
launch.execute(input).await.expect("launch succeeds");
|
||||
|
||||
let doc = convention_doc(&fs);
|
||||
// La section de reprise est la DERNIÈRE du document (ordre composeur). Son contenu
|
||||
// (après l'entête) = le summary injecté, qui doit être borné.
|
||||
let section = doc
|
||||
.split("# Reprise de la conversation\n\n")
|
||||
.nth(1)
|
||||
.expect("resume section present")
|
||||
.trim_end();
|
||||
assert!(
|
||||
section.chars().count() <= domain::HANDOFF_SUMMARY_MAX_CHARS,
|
||||
"summary legacy injecté borné, obtenu {}",
|
||||
section.chars().count()
|
||||
);
|
||||
// Distillation : les lignes les plus récentes survivent, les plus anciennes droppées.
|
||||
assert!(section.contains("tour numero 499"), "le plus récent reste");
|
||||
assert!(
|
||||
!section.contains("tour numero 0 "),
|
||||
"le plus ancien droppé : {section}"
|
||||
);
|
||||
// Reparsable : chaque ligne conservée garde le préfixe de tour.
|
||||
for line in section.lines().filter(|l| !l.is_empty()) {
|
||||
assert!(line.starts_with("- **"), "ligne reparsable : {line}");
|
||||
}
|
||||
}
|
||||
|
||||
/// LS5 — zéro régression : un handoff **normal** (sous le plafond) est injecté
|
||||
/// **verbatim** (la borne est l'identité), objectif et summary intacts.
|
||||
#[tokio::test]
|
||||
async fn launch_normal_handoff_injects_summary_verbatim() {
|
||||
let summary = "**Objectif :** livrer LS5\n\n- **Prompt:** a\n- **Response:** b";
|
||||
assert!(summary.chars().count() <= domain::HANDOFF_SUMMARY_MAX_CHARS);
|
||||
let conv = conv_id(123);
|
||||
let store = FakeHandoffStore::with(conv, handoff_for(summary, Some("livrer LS5")));
|
||||
let provider = Arc::new(FakeHandoffProvider(Arc::new(store))) as Arc<dyn HandoffProvider>;
|
||||
let (launch, agent, fs) = launch_with_handoff(Some(provider));
|
||||
|
||||
let mut input = launch_input(agent.id);
|
||||
input.conversation_id = Some(Uuid::from_u128(123).to_string());
|
||||
launch.execute(input).await.expect("launch succeeds");
|
||||
|
||||
let doc = convention_doc(&fs);
|
||||
// Le summary normal est injecté tel quel (la borne ne l'a pas touché).
|
||||
assert!(
|
||||
doc.contains(summary),
|
||||
"handoff normal injecté verbatim (borne = identité) : {doc}"
|
||||
);
|
||||
assert!(doc.contains("**Objectif :** livrer LS5"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bloc 2 — cohérence end-to-end de la clé de conversation (LE test de vérité).
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user