feat(chat): #155 paste image depuis presse-papier dans le composer custom (QA verte)

- frontend: onPaste sur CustomAgentChatView, détection MIME image, chip/preview d'attachment, envoi via le contrat #154 (adapters/agent, ports)

- backend: import d'image par bytes dans le store attachments (commands, chat_attachments app+infra, dto, ports) + tests
This commit is contained in:
2026-08-06 11:01:50 +02:00
parent 88dfbfe471
commit 790c4755be
15 changed files with 791 additions and 84 deletions

View File

@ -72,6 +72,57 @@ async fn fs_chat_attachment_store_imports_outside_file_to_project_managed_path()
let _ = tokio::fs::remove_dir_all(outside_root).await;
}
#[tokio::test]
async fn fs_chat_attachment_store_imports_bytes_to_project_managed_path() {
let project_root = temp_dir("project-bytes");
tokio::fs::create_dir_all(&project_root).await.unwrap();
let store = FsChatAttachmentStore::new();
let project_path = ProjectPath::new(project_root.to_string_lossy().into_owned()).unwrap();
let project_id = ProjectId::new_random();
let agent_id = AgentId::new_random();
let session_id = SessionId::new_random();
let attachment_id = ChatAttachmentId::new("attach-bytes").unwrap();
let attachment = store
.import_from_bytes(
&project_path,
project_id,
agent_id,
session_id,
b"clipboard png bytes",
ChatAttachmentImport {
id: attachment_id,
filename: "clipboard.png".to_owned(),
mime: "image/png".to_owned(),
source_kind: ChatAttachmentSourceKind::Clipboard,
created_at: 43,
},
)
.await
.unwrap();
assert_eq!(attachment.project_id, project_id);
assert_eq!(attachment.agent_id, agent_id);
assert_eq!(attachment.session_id, session_id);
assert_eq!(attachment.filename, "clipboard.png");
assert_eq!(attachment.size_bytes, 19);
assert_eq!(attachment.source_kind, ChatAttachmentSourceKind::Clipboard);
assert!(attachment
.readable_path
.starts_with(&format!("{}/.ideai/attachments/", project_root.display())));
let stored_bytes = tokio::fs::read(&attachment.readable_path).await.unwrap();
assert_eq!(stored_bytes, b"clipboard png bytes");
let listed = store
.list_for_session(&project_path, session_id)
.await
.unwrap();
assert_eq!(listed, vec![attachment]);
let _ = tokio::fs::remove_dir_all(project_root).await;
}
#[tokio::test]
async fn fs_chat_attachment_store_rejects_directories() {
let project_root = temp_dir("project-dir");
@ -103,3 +154,32 @@ async fn fs_chat_attachment_store_rejects_directories() {
let _ = tokio::fs::remove_dir_all(project_root).await;
let _ = tokio::fs::remove_dir_all(outside_root).await;
}
#[tokio::test]
async fn fs_chat_attachment_store_rejects_empty_bytes() {
let project_root = temp_dir("project-empty-bytes");
tokio::fs::create_dir_all(&project_root).await.unwrap();
let store = FsChatAttachmentStore::new();
let err = store
.import_from_bytes(
&ProjectPath::new(project_root.to_string_lossy().into_owned()).unwrap(),
ProjectId::new_random(),
AgentId::new_random(),
SessionId::new_random(),
&[],
ChatAttachmentImport {
id: ChatAttachmentId::new("attach-empty").unwrap(),
filename: "empty.png".to_owned(),
mime: "image/png".to_owned(),
source_kind: ChatAttachmentSourceKind::Clipboard,
created_at: 42,
},
)
.await
.unwrap_err();
assert!(matches!(err, ChatAttachmentStoreError::Invalid(_)));
let _ = tokio::fs::remove_dir_all(project_root).await;
}