fix(chat): le transcript s'affiche à nouveau après redémarrage automatique d'un agent — #295 (app-tauri ChatBridge: cartes successors/owners, rebind_session déplace sink+scrolloback du logical chat vers la session de remplacement et canonical_session résout les écritures avec l'ancien id sur toutes les opérations register/scrollback/detach/send_output; backend stream: ReplayOutputBridge::rekey prépare le scrollback source devant le destination et invalide les générations pré-rekey des deux pumps; commands: agent_send binde l'owner stable (projectId,agentId) au tour humain et rebind avant le flux de continuation au handoff restart; reattach_agent_chat résout le successeur du bridge puis fallback identité stable projectId/agentId explicite ou owner bindé, renvoie sessionId effectif + conversationId, NOT_FOUND sinon; web-server chat.attach même fallback stale→live par project/agent; DTO ReattachChatDto/ReattachResultDto documentent l'id effectif post-restart; frontend: ReattachAgentChatOptions, conversationId dans ChatAttachedPayload, CustomAgentChatView réattache sur agentRestartCompleted (dédupe par restartId, scrollback appliqué, tours restart préservés), retry identité stable sur NOT_FOUND dans openOrAttach avant fallback launch frais, publication sessionId effectif + conversationId, état vide «Conversation liée» quand une conversation/run/workstate lié existe; QA verte reconfirmée par Git: git diff --check, cargo fmt --all --check, app-tauri chat_bridge 34/34 dont 3 nouveaux restart_rebind + dto_chat 24/24, cargo check -p app-tauri, backend 91+7 ignored permissions socket, web-server websocket_chat_attach 3/3, npm test 77/77 CustomAgentChatView+agentGateway, npm run build; réserves non bloquantes: warnings Vite esbuild/oxc et chunk >500kB connus)
This commit is contained in:
@ -550,6 +550,84 @@ fn reattach_repaints_scrollback_and_preserves_it_without_resend() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restart_rebind_preserves_transcript_and_routes_old_id_writes_to_replacement() {
|
||||
let bridge = ChatBridge::new();
|
||||
let previous = sid();
|
||||
let replacement = sid();
|
||||
let sink = Arc::new(Mutex::new(Vec::new()));
|
||||
let generation = bridge.register(previous, capturing_channel(Arc::clone(&sink)));
|
||||
|
||||
assert!(bridge.send_output(&previous, delta("before restart")));
|
||||
bridge.rebind_session(previous, replacement);
|
||||
|
||||
assert_eq!(bridge.successor(&previous), replacement);
|
||||
assert_eq!(
|
||||
bridge.scrollback(&replacement),
|
||||
vec![delta("before restart")]
|
||||
);
|
||||
assert!(
|
||||
bridge.send_output(&previous, delta("after restart")),
|
||||
"the in-flight old pump must reach the rekeyed live sink"
|
||||
);
|
||||
assert_eq!(
|
||||
bridge.scrollback(&replacement),
|
||||
vec![delta("before restart"), delta("after restart")]
|
||||
);
|
||||
assert_eq!(sink.lock().unwrap().len(), 2);
|
||||
|
||||
bridge.detach_if(&previous, generation);
|
||||
assert!(
|
||||
!bridge.send_output(&replacement, delta("detached")),
|
||||
"the old pump generation detaches the migrated sink"
|
||||
);
|
||||
assert_eq!(bridge.scrollback(&previous).len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restart_rebind_moves_stable_owner_to_the_replacement_session() {
|
||||
let bridge = ChatBridge::new();
|
||||
let previous = sid();
|
||||
let replacement = sid();
|
||||
let project = domain::ProjectId::from_uuid(Uuid::from_u128(10));
|
||||
let agent = domain::AgentId::from_uuid(Uuid::from_u128(20));
|
||||
bridge.bind_owner(previous, project, agent);
|
||||
|
||||
bridge.rebind_session(previous, replacement);
|
||||
|
||||
assert_eq!(bridge.owner(&previous), Some((project, agent)));
|
||||
assert_eq!(bridge.owner(&replacement), Some((project, agent)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restart_rebind_keeps_a_newer_replacement_sink_and_merges_scrollback_in_order() {
|
||||
let bridge = ChatBridge::new();
|
||||
let previous = sid();
|
||||
let replacement = sid();
|
||||
let old_sink = Arc::new(Mutex::new(Vec::new()));
|
||||
let new_sink = Arc::new(Mutex::new(Vec::new()));
|
||||
let old_generation = bridge.register(previous, capturing_channel(Arc::clone(&old_sink)));
|
||||
assert!(bridge.send_output(&previous, delta("old")));
|
||||
bridge.register(replacement, capturing_channel(Arc::clone(&new_sink)));
|
||||
assert!(bridge.send_output(&replacement, delta("new")));
|
||||
|
||||
bridge.rebind_session(previous, replacement);
|
||||
|
||||
assert_eq!(
|
||||
bridge.scrollback(&replacement),
|
||||
vec![delta("old"), delta("new")]
|
||||
);
|
||||
assert!(bridge.send_output(&previous, delta("continued")));
|
||||
assert_eq!(old_sink.lock().unwrap().len(), 1);
|
||||
assert_eq!(new_sink.lock().unwrap().len(), 2);
|
||||
|
||||
bridge.detach_if(&previous, old_generation);
|
||||
assert!(
|
||||
bridge.send_output(&replacement, delta("still attached")),
|
||||
"the old pump must not detach a channel already attached to the replacement"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detach_view_keeps_session_scrollback_and_later_reattach_replays_absent_chunks() {
|
||||
let bridge = ChatBridge::new();
|
||||
|
||||
Reference in New Issue
Block a user