feat(tickets): ajoute les pièces jointes sur tickets (#108)

Stockage flat côté ticket + métadonnées d'attachments, lecture exposée côté
backend/MCP, et UI minimale de liste/ajout dans TicketDetail. Traverse le
domaine (Issue, ports), l'application (usecases + assistant de ticket), les
adaptateurs infra/MCP (issues store, orchestrateur), les DTO backend/web-
server/app-tauri, et le frontend (domain/ports/adapters/hooks/UI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 11:08:45 +02:00
parent 2692b9cc03
commit 8158057b1d
34 changed files with 1722 additions and 102 deletions

View File

@ -2,9 +2,9 @@ use std::path::PathBuf;
use std::str::FromStr;
use domain::{
AgentId, AgentIssueRef, AgentIssueRole, Issue, IssueActor, IssueId, IssueListFilter,
IssueNumberAllocator, IssuePriority, IssueRef, IssueStatus, IssueStore, IssueStoreError,
MarkdownDoc, ProjectPath, SprintId,
AgentId, AgentIssueRef, AgentIssueRole, Issue, IssueActor, IssueAttachmentId, IssueId,
IssueListFilter, IssueNumberAllocator, IssuePriority, IssueRef, IssueStatus, IssueStore,
IssueStoreError, LocalPath, MarkdownDoc, ProjectPath, SprintId,
};
use infrastructure::{FsIssueNumberAllocator, FsIssueStore};
use uuid::Uuid;
@ -402,6 +402,66 @@ async fn issue_store_persists_and_filters_sprint_membership() {
assert_eq!(rows[0].sprint, Some(sprint_id));
}
#[tokio::test]
async fn issue_store_adds_reads_and_marks_attachment_summarized() {
let tmp = TempDir::new();
let root = tmp.root();
let store = FsIssueStore::new();
let issue = issue(&root, 7, "Attachment ticket");
store.create(&root, &issue).await.unwrap();
let source = tmp.child("source-note.txt");
std::fs::write(&source, "attachment payload").unwrap();
let attachment_id = IssueAttachmentId::new("att_1".to_owned()).unwrap();
let updated = store
.add_attachment_from_path(
&root,
issue.reference(),
&LocalPath::new(source.to_string_lossy().to_string()),
attachment_id.clone(),
"source-note.txt".to_owned(),
"text/plain".to_owned(),
IssueActor::User,
2_000,
issue.version,
)
.await
.unwrap();
assert_eq!(updated.attachments.len(), 1);
assert_eq!(updated.attachments[0].filename, "source-note.txt");
assert_eq!(updated.attachments[0].mime, "text/plain");
assert_eq!(updated.attachments[0].size_bytes, 18);
assert!(!updated.attachments[0].summarized_in_carnet);
assert!(tmp
.child(".ideai/tickets/7/attachments/att_1-source-note.txt")
.exists());
let content = store
.read_attachment(&root, issue.reference(), &attachment_id)
.await
.unwrap();
assert_eq!(content.attachment.id, attachment_id);
assert_eq!(content.bytes, b"attachment payload");
let summarized = store
.mark_attachment_summarized(
&root,
issue.reference(),
&attachment_id,
IssueActor::System,
3_000,
updated.version,
)
.await
.unwrap();
let attachment = &summarized.attachments[0];
assert!(attachment.summarized_in_carnet);
assert_eq!(attachment.summarized_by, Some(IssueActor::System));
assert_eq!(attachment.summarized_at, Some(3_000));
}
#[tokio::test]
async fn issue_store_delete_removes_files_and_index_entry() {
let tmp = TempDir::new();