Ajoute created_by sur IssueIndexEntry/TicketSummaryDto + filtre createdBy (#5), et les use cases BulkUpdateIssueStatus/Priority + BulkDeleteIssues avec les DTO/outils MCP idea_ticket_bulk_* associés (#6). NON VERT : BulkDeleteIssues::execute référence BatchIssueResult::deleted(), constructeur pas encore ajouté à BatchIssueResult (seuls ok/err existent) -> `cargo check` échoue (E0599 dans application::issues::mod). Reste à DevBackend avant toute QA/merge. Branché sur feature/sdk-integration (dépend du commit #2 dans orchestrator/mcp/server.rs et tools.rs) ; à rebaser sur develop une fois feature/sdk-integration mergé. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use async_trait::async_trait;
|
||||
@ -14,7 +15,9 @@ use domain::{
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::{
|
||||
CreateIssue, CreateIssueInput, DeleteIssue, DeleteIssueInput, UpdateIssue, UpdateIssueInput,
|
||||
BulkDeleteIssues, BulkDeleteIssuesInput, BulkUpdateIssuePriority, BulkUpdateIssuePriorityInput,
|
||||
BulkUpdateIssueStatus, BulkUpdateIssueStatusInput, CreateIssue, CreateIssueInput, DeleteIssue,
|
||||
DeleteIssueInput, UpdateIssue, UpdateIssueInput,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
@ -504,3 +507,163 @@ async fn delete_issue_emits_deleted_event_without_freed_sprint() {
|
||||
}] if *issue_ref == initial.reference()
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bulk_update_status_preserves_order_and_allows_partial_success() {
|
||||
let project = project();
|
||||
let issues = Arc::new(FakeIssues::default());
|
||||
for number in [1, 3] {
|
||||
let issue = Issue::new(
|
||||
domain::IssueId::new_random(),
|
||||
IssueNumber::new(number).unwrap(),
|
||||
format!("Ticket {number}"),
|
||||
MarkdownDoc::new("Body"),
|
||||
IssueStatus::Open,
|
||||
IssuePriority::Medium,
|
||||
MarkdownDoc::default(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
IssueActor::User,
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
issues.create(&project.root, &issue).await.unwrap();
|
||||
}
|
||||
let bus = Arc::new(SpyBus::default());
|
||||
let uc = BulkUpdateIssueStatus::new(
|
||||
issues.clone(),
|
||||
Arc::new(FixedClock),
|
||||
bus.clone(),
|
||||
);
|
||||
|
||||
let out = uc
|
||||
.execute(BulkUpdateIssueStatusInput {
|
||||
project: project.clone(),
|
||||
issue_refs: vec![
|
||||
IssueRef::from_str("#1").unwrap(),
|
||||
IssueRef::from_str("#2").unwrap(),
|
||||
IssueRef::from_str("#3").unwrap(),
|
||||
],
|
||||
status: IssueStatus::Qa,
|
||||
actor: IssueActor::System,
|
||||
})
|
||||
.await;
|
||||
|
||||
let refs: Vec<String> = out
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| item.issue_ref.to_string())
|
||||
.collect();
|
||||
assert_eq!(refs, ["#1", "#2", "#3"]);
|
||||
assert_eq!(out.items[0].issue.as_ref().unwrap().status, IssueStatus::Qa);
|
||||
assert_eq!(out.items[1].error.as_ref().unwrap().code(), "NOT_FOUND");
|
||||
assert_eq!(out.items[2].issue.as_ref().unwrap().status, IssueStatus::Qa);
|
||||
assert_eq!(
|
||||
bus.events()
|
||||
.iter()
|
||||
.filter(|event| matches!(event, DomainEvent::IssueStatusChanged { .. }))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bulk_update_priority_updates_each_existing_ticket() {
|
||||
let project = project();
|
||||
let issues = Arc::new(FakeIssues::default());
|
||||
let initial = Issue::new(
|
||||
domain::IssueId::new_random(),
|
||||
IssueNumber::new(1).unwrap(),
|
||||
"Initial",
|
||||
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 = BulkUpdateIssuePriority::new(
|
||||
issues.clone(),
|
||||
Arc::new(FixedClock),
|
||||
bus.clone(),
|
||||
);
|
||||
|
||||
let out = uc
|
||||
.execute(BulkUpdateIssuePriorityInput {
|
||||
project,
|
||||
issue_refs: vec![initial.reference()],
|
||||
priority: IssuePriority::Critical,
|
||||
actor: IssueActor::System,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(out.items.len(), 1);
|
||||
assert!(out.items[0].error.is_none());
|
||||
assert_eq!(
|
||||
out.items[0].issue.as_ref().unwrap().priority,
|
||||
IssuePriority::Critical
|
||||
);
|
||||
assert!(bus.events().iter().any(|event| matches!(
|
||||
event,
|
||||
DomainEvent::IssuePriorityChanged {
|
||||
priority: IssuePriority::Critical,
|
||||
..
|
||||
}
|
||||
)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bulk_delete_preserves_order_and_allows_partial_success() {
|
||||
let project = project();
|
||||
let issues = Arc::new(FakeIssues::default());
|
||||
let initial = Issue::new(
|
||||
domain::IssueId::new_random(),
|
||||
IssueNumber::new(1).unwrap(),
|
||||
"Initial",
|
||||
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 = BulkDeleteIssues::new(issues.clone(), bus.clone());
|
||||
|
||||
let out = uc
|
||||
.execute(BulkDeleteIssuesInput {
|
||||
project: project.clone(),
|
||||
issue_refs: vec![IssueRef::from_str("#2").unwrap(), initial.reference()],
|
||||
})
|
||||
.await;
|
||||
|
||||
let refs: Vec<String> = out
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| item.issue_ref.to_string())
|
||||
.collect();
|
||||
assert_eq!(refs, ["#2", "#1"]);
|
||||
assert_eq!(out.items[0].error.as_ref().unwrap().code(), "NOT_FOUND");
|
||||
assert!(out.items[1].error.is_none());
|
||||
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: None,
|
||||
..
|
||||
}] if *issue_ref == initial.reference()
|
||||
));
|
||||
}
|
||||
|
||||
@ -9,7 +9,8 @@ use async_trait::async_trait;
|
||||
use application::{
|
||||
DeleteModelArtifact, DeleteModelArtifactInput, DeleteModelServer, DeleteModelServerInput,
|
||||
EnsureLocalModelServer, EnsureLocalModelServerInput, LiveAgentRegistry, LiveSessionKind,
|
||||
LiveSessionSnapshot, ModelArtifactDownloadTracker, ModelServerReadinessPolicy, TerminalSessions,
|
||||
LiveSessionSnapshot, ModelArtifactDownloadTracker, ModelServerReadinessPolicy,
|
||||
TerminalSessions,
|
||||
};
|
||||
use domain::events::DomainEvent;
|
||||
use domain::layout::Workspace;
|
||||
|
||||
Reference in New Issue
Block a user