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.
168 lines
4.9 KiB
Rust
168 lines
4.9 KiB
Rust
use std::sync::{Arc, Mutex};
|
|
|
|
use async_trait::async_trait;
|
|
use domain::ports::{
|
|
ChatAttachmentImport, ChatAttachmentStore, ChatAttachmentStoreError, Clock, IdGenerator,
|
|
};
|
|
use domain::{
|
|
AgentId, ChatAttachment, ChatAttachmentSourceKind, LocalPath, Project, ProjectId, ProjectPath,
|
|
RemoteRef, SessionId,
|
|
};
|
|
use uuid::Uuid;
|
|
|
|
use application::{ImportChatAttachmentItem, ImportChatAttachments, ImportChatAttachmentsInput};
|
|
|
|
#[derive(Default)]
|
|
struct FakeChatAttachments {
|
|
calls: Mutex<Vec<(LocalPath, ChatAttachmentImport)>>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ChatAttachmentStore for FakeChatAttachments {
|
|
async fn import_from_path(
|
|
&self,
|
|
root: &ProjectPath,
|
|
project_id: ProjectId,
|
|
agent_id: AgentId,
|
|
session_id: SessionId,
|
|
source: &LocalPath,
|
|
import: ChatAttachmentImport,
|
|
) -> Result<ChatAttachment, ChatAttachmentStoreError> {
|
|
self.calls
|
|
.lock()
|
|
.unwrap()
|
|
.push((source.clone(), import.clone()));
|
|
let storage_path = format!(
|
|
"agent-chat/{session_id}/{}-{}",
|
|
import.id.as_str(),
|
|
import.filename
|
|
);
|
|
let readable_path = format!(
|
|
"{}/.ideai/attachments/agent-chat/{session_id}/{}-{}",
|
|
root.as_str(),
|
|
import.id.as_str(),
|
|
import.filename
|
|
);
|
|
Ok(ChatAttachment {
|
|
id: import.id,
|
|
project_id,
|
|
agent_id,
|
|
session_id,
|
|
filename: import.filename.clone(),
|
|
mime: import.mime.clone(),
|
|
size_bytes: 12,
|
|
source_kind: import.source_kind,
|
|
storage_path,
|
|
readable_path,
|
|
created_at: import.created_at,
|
|
})
|
|
}
|
|
|
|
async fn list_for_session(
|
|
&self,
|
|
_root: &ProjectPath,
|
|
_session_id: SessionId,
|
|
) -> Result<Vec<ChatAttachment>, ChatAttachmentStoreError> {
|
|
Ok(Vec::new())
|
|
}
|
|
}
|
|
|
|
struct FixedClock;
|
|
|
|
impl Clock for FixedClock {
|
|
fn now_millis(&self) -> i64 {
|
|
1_234
|
|
}
|
|
}
|
|
|
|
struct FixedIds;
|
|
|
|
impl IdGenerator for FixedIds {
|
|
fn new_uuid(&self) -> Uuid {
|
|
Uuid::parse_str("11111111-2222-3333-4444-555555555555").unwrap()
|
|
}
|
|
}
|
|
|
|
fn project() -> Project {
|
|
Project::new(
|
|
ProjectId::from_uuid(Uuid::parse_str("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa").unwrap()),
|
|
"P",
|
|
ProjectPath::new("/tmp/idea-chat-attachments-test").unwrap(),
|
|
RemoteRef::Local,
|
|
0,
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn import_chat_attachments_allocates_metadata_and_uses_store_port() {
|
|
let store = Arc::new(FakeChatAttachments::default());
|
|
let usecase = ImportChatAttachments::new(
|
|
Arc::clone(&store) as Arc<dyn ChatAttachmentStore>,
|
|
Arc::new(FixedIds),
|
|
Arc::new(FixedClock),
|
|
);
|
|
let agent_id =
|
|
AgentId::from_uuid(Uuid::parse_str("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb").unwrap());
|
|
let session_id =
|
|
SessionId::from_uuid(Uuid::parse_str("cccccccc-cccc-cccc-cccc-cccccccccccc").unwrap());
|
|
|
|
let output = usecase
|
|
.execute(ImportChatAttachmentsInput {
|
|
project: project(),
|
|
agent_id,
|
|
session_id,
|
|
attachments: vec![ImportChatAttachmentItem {
|
|
path: "/outside/photo.png".to_owned(),
|
|
mime: None,
|
|
source_kind: ChatAttachmentSourceKind::Clipboard,
|
|
}],
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(output.attachments.len(), 1);
|
|
let attachment = &output.attachments[0];
|
|
assert_eq!(attachment.filename, "photo.png");
|
|
assert_eq!(attachment.mime, "image/png");
|
|
assert_eq!(attachment.source_kind, ChatAttachmentSourceKind::Clipboard);
|
|
assert_eq!(attachment.created_at, 1_234);
|
|
assert!(attachment
|
|
.storage_path
|
|
.starts_with("agent-chat/cccccccc-cccc-cccc-cccc-cccccccccccc/"));
|
|
assert!(attachment
|
|
.readable_path
|
|
.contains("/.ideai/attachments/agent-chat/"));
|
|
|
|
let calls = store.calls.lock().unwrap();
|
|
assert_eq!(calls.len(), 1);
|
|
assert_eq!(calls[0].0.as_str(), "/outside/photo.png");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn import_chat_attachments_rejects_executable_sources_before_store_call() {
|
|
let store = Arc::new(FakeChatAttachments::default());
|
|
let usecase = ImportChatAttachments::new(
|
|
Arc::clone(&store) as Arc<dyn ChatAttachmentStore>,
|
|
Arc::new(FixedIds),
|
|
Arc::new(FixedClock),
|
|
);
|
|
|
|
let err = usecase
|
|
.execute(ImportChatAttachmentsInput {
|
|
project: project(),
|
|
agent_id: AgentId::new_random(),
|
|
session_id: SessionId::new_random(),
|
|
attachments: vec![ImportChatAttachmentItem {
|
|
path: "/outside/run.sh".to_owned(),
|
|
mime: None,
|
|
source_kind: ChatAttachmentSourceKind::LocalFile,
|
|
}],
|
|
})
|
|
.await
|
|
.unwrap_err();
|
|
|
|
assert_eq!(err.code(), "INVALID");
|
|
assert!(store.calls.lock().unwrap().is_empty());
|
|
}
|