feat(attachments): fondation pipeline durable d'attachments agent/chat + sandbox-safe (#154, QA verte)

Socle #154 : entité attachment, store, contrat DTO et routage session. Modules : domain/chat_attachment, application/chat_attachments, infrastructure/chat_attachments, app-tauri (commands/lib), backend dto.
This commit is contained in:
2026-08-06 10:44:15 +02:00
parent fb19ee48dc
commit 1c80d08f92
16 changed files with 1210 additions and 158 deletions

View File

@ -6,11 +6,14 @@
//! - non-regression: every `TerminalSessionDto` construction path now serialises a
//! `cellKind` and PTY paths keep `"pty"`.
use app_tauri_lib::dto::{CellKind, ReattachChatDto, ReplyChunk, TerminalSessionDto};
use app_tauri_lib::dto::{
CellKind, ChatAttachmentDto, ChatAttachmentInputDto, ImportChatAttachmentsRequestDto,
ImportChatAttachmentsResponseDto, ReattachChatDto, ReplyChunk, TerminalSessionDto,
};
use application::{LaunchAgentOutput, StructuredSessionDescriptor};
use domain::project::ProjectPath;
use domain::SessionId;
use domain::{AgentId, NodeId, PtySize, SessionKind, SessionStatus, TerminalSession};
use domain::{ChatAttachmentSourceKind, SessionId};
use serde_json::json;
use uuid::Uuid;
@ -103,6 +106,84 @@ fn reply_chunk_rejects_snake_case_tag() {
assert!(r.is_err(), "snake_case kind must not deserialise");
}
// ---------------------------------------------------------------------------
// Chat attachments — structured input + durable metadata, camelCase
// ---------------------------------------------------------------------------
#[test]
fn chat_attachment_input_dto_deserialises_camel_case() {
let dto: ChatAttachmentInputDto = serde_json::from_value(json!({
"path": "/tmp/picked.png",
"mime": "image/png",
"sourceKind": "clipboard"
}))
.unwrap();
assert_eq!(dto.path, "/tmp/picked.png");
assert_eq!(dto.mime.as_deref(), Some("image/png"));
assert_eq!(dto.source_kind, Some(ChatAttachmentSourceKind::Clipboard));
}
#[test]
fn chat_attachment_dto_serialises_camel_case() {
let dto = ChatAttachmentDto {
id: "attach-1".to_owned(),
filename: "picked.png".to_owned(),
mime: "image/png".to_owned(),
size_bytes: 9,
source_kind: ChatAttachmentSourceKind::LocalFile,
storage_path: "agent-chat/session/attach-1-picked.png".to_owned(),
readable_path: "/project/.ideai/attachments/agent-chat/session/attach-1-picked.png"
.to_owned(),
created_at: 42,
};
let v = serde_json::to_value(dto).unwrap();
assert_eq!(v["sizeBytes"], 9);
assert_eq!(v["sourceKind"], "localFile");
assert_eq!(v["storagePath"], "agent-chat/session/attach-1-picked.png");
assert!(v.get("readable_path").is_none(), "no snake_case leak");
}
#[test]
fn import_chat_attachments_request_response_use_camel_case() {
let request: ImportChatAttachmentsRequestDto = serde_json::from_value(json!({
"sessionId": "sess-1",
"attachmentPaths": ["/tmp/legacy.txt"],
"attachments": [{
"path": "/tmp/picked.png",
"mime": "image/png",
"sourceKind": "dragDrop"
}]
}))
.unwrap();
assert_eq!(request.session_id, "sess-1");
assert_eq!(request.attachment_paths, vec!["/tmp/legacy.txt"]);
assert_eq!(
request.attachments[0].source_kind,
Some(ChatAttachmentSourceKind::DragDrop)
);
let response = ImportChatAttachmentsResponseDto {
attachments: vec![ChatAttachmentDto {
id: "attach-1".to_owned(),
filename: "picked.png".to_owned(),
mime: "image/png".to_owned(),
size_bytes: 9,
source_kind: ChatAttachmentSourceKind::LocalFile,
storage_path: "agent-chat/session/attach-1-picked.png".to_owned(),
readable_path: "/project/.ideai/attachments/agent-chat/session/attach-1-picked.png"
.to_owned(),
created_at: 42,
}],
};
let v = serde_json::to_value(response).unwrap();
assert_eq!(v["attachments"][0]["sizeBytes"], 9);
assert!(v.get("attachment_paths").is_none(), "no snake_case leak");
}
// ---------------------------------------------------------------------------
// ReattachChatDto — typed scrollback, camelCase (zone 5)
// ---------------------------------------------------------------------------