feat(tickets): suppression d'un ticket (backend + frontend)
Ajoute la suppression complète d'un ticket (#6). Backend Rust : - port IssueStore::delete (NotFound si absent) + event IssueDeleted{freed_sprint} - FsIssueStore::delete : supprime .ideai/tickets/<N>/ et l'index sous lock - use case DeleteIssue + adaptations des tests sprint/ticket_assistant au port - commande Tauri ticket_delete + câblage events/state/lib Frontend : - gateway delete (ports, adapter ticket + mock, domain) - useTicketDetail : retrait de la liste et fermeture du détail via event issueDeleted - intégration TicketDetail + tests Tests verts : application/issue_usecases (6), infrastructure/issue_store (7), app-tauri --lib (56), frontend vitest (503), npm run build (exit 0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -13,7 +13,9 @@ use domain::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::{CreateIssue, CreateIssueInput, UpdateIssue, UpdateIssueInput};
|
||||
use application::{
|
||||
CreateIssue, CreateIssueInput, DeleteIssue, DeleteIssueInput, UpdateIssue, UpdateIssueInput,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeIssues {
|
||||
@ -84,6 +86,19 @@ impl IssueStore for FakeIssues {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
issue_ref: IssueRef,
|
||||
) -> Result<(), IssueStoreError> {
|
||||
self.issues
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&issue_ref)
|
||||
.map(|_| ())
|
||||
.ok_or(IssueStoreError::NotFound)
|
||||
}
|
||||
|
||||
async fn read_carnet(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
@ -400,3 +415,92 @@ async fn update_issue_maps_version_conflict() {
|
||||
assert_eq!(err.code(), "INVALID");
|
||||
assert!(err.to_string().contains("version conflict"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_issue_emits_deleted_event_with_freed_sprint() {
|
||||
let issues = Arc::new(FakeIssues::default());
|
||||
let sprint_id = domain::SprintId::from_uuid(Uuid::from_u128(7));
|
||||
let initial = Issue::new(
|
||||
domain::IssueId::new_random(),
|
||||
IssueNumber::new(1).unwrap(),
|
||||
"In sprint",
|
||||
MarkdownDoc::new("Body"),
|
||||
IssueStatus::Open,
|
||||
IssuePriority::Medium,
|
||||
MarkdownDoc::default(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
IssueActor::User,
|
||||
1,
|
||||
)
|
||||
.unwrap()
|
||||
.mutate(IssueActor::System, 2, |issue| {
|
||||
issue.sprint = Some(sprint_id);
|
||||
})
|
||||
.unwrap();
|
||||
issues.create(&project().root, &initial).await.unwrap();
|
||||
let bus = Arc::new(SpyBus::default());
|
||||
let uc = DeleteIssue::new(issues.clone(), bus.clone());
|
||||
|
||||
let out = uc
|
||||
.execute(DeleteIssueInput {
|
||||
project: project(),
|
||||
issue_ref: initial.reference(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(out.issue_ref, initial.reference());
|
||||
assert!(matches!(
|
||||
issues
|
||||
.get_by_ref(&project().root, initial.reference())
|
||||
.await,
|
||||
Err(IssueStoreError::NotFound)
|
||||
));
|
||||
assert!(matches!(
|
||||
bus.events().as_slice(),
|
||||
[DomainEvent::IssueDeleted {
|
||||
issue_ref,
|
||||
freed_sprint: Some(id),
|
||||
..
|
||||
}] if *issue_ref == initial.reference() && *id == sprint_id
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_issue_emits_deleted_event_without_freed_sprint() {
|
||||
let issues = Arc::new(FakeIssues::default());
|
||||
let initial = Issue::new(
|
||||
domain::IssueId::new_random(),
|
||||
IssueNumber::new(2).unwrap(),
|
||||
"Backlog",
|
||||
MarkdownDoc::new("Body"),
|
||||
IssueStatus::Open,
|
||||
IssuePriority::Medium,
|
||||
MarkdownDoc::default(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
IssueActor::User,
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
issues.create(&project().root, &initial).await.unwrap();
|
||||
let bus = Arc::new(SpyBus::default());
|
||||
let uc = DeleteIssue::new(issues, bus.clone());
|
||||
|
||||
uc.execute(DeleteIssueInput {
|
||||
project: project(),
|
||||
issue_ref: initial.reference(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
bus.events().as_slice(),
|
||||
[DomainEvent::IssueDeleted {
|
||||
issue_ref,
|
||||
freed_sprint: None,
|
||||
..
|
||||
}] if *issue_ref == initial.reference()
|
||||
));
|
||||
}
|
||||
|
||||
@ -159,6 +159,19 @@ impl IssueStore for FakeIssues {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
issue_ref: IssueRef,
|
||||
) -> Result<(), IssueStoreError> {
|
||||
self.issues
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&issue_ref)
|
||||
.map(|_| ())
|
||||
.ok_or(IssueStoreError::NotFound)
|
||||
}
|
||||
|
||||
async fn read_carnet(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
|
||||
@ -106,6 +106,14 @@ impl IssueStore for FakeIssues {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn delete(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
_issue_ref: IssueRef,
|
||||
) -> Result<(), IssueStoreError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn read_carnet(
|
||||
&self,
|
||||
_root: &ProjectPath,
|
||||
|
||||
Reference in New Issue
Block a user