bulk operations fixes

- Frontend: implémente bulkDelete, bulkArchive, bulkRestore
- Backend: adapts ticket DTOs pour bulk operations
- Infrastructure: MCP server pour gestion bulk tickets
- Tests: coverage frontend + backend
This commit is contained in:
2026-07-28 20:38:07 +02:00
parent 73c6081ab8
commit bd4f76ae74
19 changed files with 597 additions and 35 deletions

View File

@ -280,6 +280,7 @@ pub fn run() {
tickets::ticket_delete,
tickets::ticket_bulk_update_status,
tickets::ticket_bulk_update_priority,
tickets::ticket_bulk_delete,
tickets::open_ticket_chat,
tickets::close_ticket_chat,
tickets::ticket_list,

View File

@ -344,7 +344,9 @@ fn is_ticket_policy_mutation_tool(name: &str) -> bool {
fn is_ticket_policy_bulk_mutation_tool(name: &str) -> bool {
matches!(
name,
"idea_ticket_bulk_update_status" | "idea_ticket_bulk_update_priority"
"idea_ticket_bulk_update_status"
| "idea_ticket_bulk_update_priority"
| "idea_ticket_bulk_delete"
)
}

View File

@ -68,6 +68,7 @@ impl AppState {
update: Arc::clone(&core.update_issue),
bulk_update_status: Arc::clone(&core.bulk_update_issue_status),
bulk_update_priority: Arc::clone(&core.bulk_update_issue_priority),
bulk_delete: Arc::clone(&core.bulk_delete_issues),
read_carnet: Arc::clone(&core.read_issue_carnet),
update_carnet: Arc::clone(&core.update_issue_carnet),
link: Arc::clone(&core.link_issues),

View File

@ -31,6 +31,7 @@ pub struct AppTicketToolProvider {
pub update: Arc<application::UpdateIssue>,
pub bulk_update_status: Arc<application::BulkUpdateIssueStatus>,
pub bulk_update_priority: Arc<application::BulkUpdateIssuePriority>,
pub bulk_delete: Arc<application::BulkDeleteIssues>,
pub read_carnet: Arc<application::ReadIssueCarnet>,
pub update_carnet: Arc<application::UpdateIssueCarnet>,
pub link: Arc<application::LinkIssues>,
@ -204,6 +205,14 @@ impl TicketToolProvider for AppTicketToolProvider {
.await;
json!(TicketBulkResultDto::from(out))
}
"idea_ticket_bulk_delete" => {
let req = mcp_bulk_delete_request(project, arguments)?;
let out = self
.bulk_delete
.execute(bulk_delete_input(project.clone(), req).map_err(dto_tool_error)?)
.await;
json!(TicketBulkResultDto::from(out))
}
"idea_ticket_read_carnet" => {
let carnet = self
.read_carnet
@ -390,6 +399,19 @@ pub async fn ticket_bulk_update_priority(
Ok(TicketBulkResultDto::from(out))
}
#[tauri::command]
pub async fn ticket_bulk_delete(
request: TicketBulkDeleteRequestDto,
state: State<'_, AppState>,
) -> Result<TicketBulkResultDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let out = state
.bulk_delete_issues
.execute(bulk_delete_input(project, request)?)
.await;
Ok(TicketBulkResultDto::from(out))
}
#[tauri::command]
pub async fn open_ticket_chat(
request: OpenTicketChatRequestDto,
@ -844,6 +866,16 @@ fn mcp_bulk_update_priority_request(
Ok(req)
}
fn mcp_bulk_delete_request(
project: &Project,
arguments: Value,
) -> Result<TicketBulkDeleteRequestDto, TicketToolError> {
let mut req: TicketBulkDeleteRequestDto = serde_json::from_value(arguments)
.map_err(|e| TicketToolError::new("invalid", e.to_string()))?;
req.project_id = project.id.to_string();
Ok(req)
}
fn parse_json_ref(arguments: &Value, key: &str) -> Result<IssueRef, TicketToolError> {
required_str(arguments, key).and_then(|raw| parse_ref_dto(raw).map_err(dto_tool_error))
}

View File

@ -14,26 +14,26 @@ use std::sync::{Arc, Mutex};
use application::{
AgentResumer, AgentWakeService, AppError, AssignIssueAgent, AssignSkillToAgent,
AssignTicketToSprint, AttachLiveAgent, AuthenticateSession, BackgroundCommandArchive,
BulkUpdateIssuePriority, BulkUpdateIssueStatus, CancelBackgroundTask, ChangeAgentProfile,
CheckEmbedderSuggestion, CloneOpenCodeProfileFromSeed, CloneProfileFromSeed, CloseProject,
CloseTab, CloseTerminal, CloseTicketAssistant, ConfigureProfiles, ContextGuardUseCases,
CreateAgentFromScratch, CreateAgentFromTemplate, CreateIssue, CreateLayout, CreateMemory,
CreateProject, CreateSkill, CreateSprint, CreateTemplate, DeleteAgent, DeleteEmbedderProfile,
DeleteIssue, DeleteLayout, DeleteMemory, DeleteModelArtifact, DeleteModelServer, DeleteProfile,
DeleteSkill, DeleteSprint, DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift,
DetectProfiles, DismissEmbedderSuggestion, EnsureLocalModelServer, FirstRunState,
GetAppExitWorkGuardState, GetLiveStateLean, GetMemory, GetProjectPermissions,
GetProjectSystemPermissions, GetProjectWorkState, GitBranches, GitCheckout, GitCommit,
GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage, HarvestMemoryFromTurn,
HealthUseCase, InspectConversation, InstallPluginFromArchive, InstallPluginFromDirectory,
JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput, LinkIssues, ListAgents,
ListAgentsInput, ListClaudeModels, ListCodexModels, ListDevices, ListEmbedderProfiles,
ListIssues, ListLayouts, ListMemories, ListModelServers, ListOpenCodeProviders,
ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects, ListResumableAgents,
ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions, LiveStateLeanProvider,
LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime, McpToolPermissionCatalogue,
MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice,
BulkDeleteIssues, BulkUpdateIssuePriority, BulkUpdateIssueStatus, CancelBackgroundTask,
ChangeAgentProfile, CheckEmbedderSuggestion, CloneOpenCodeProfileFromSeed,
CloneProfileFromSeed, CloseProject, CloseTab, CloseTerminal, CloseTicketAssistant,
ConfigureProfiles, ContextGuardUseCases, CreateAgentFromScratch, CreateAgentFromTemplate,
CreateIssue, CreateLayout, CreateMemory, CreateProject, CreateSkill, CreateSprint,
CreateTemplate, DeleteAgent, DeleteEmbedderProfile, DeleteIssue, DeleteLayout, DeleteMemory,
DeleteModelArtifact, DeleteModelServer, DeleteProfile, DeleteSkill, DeleteSprint,
DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift, DetectProfiles,
DismissEmbedderSuggestion, EnsureLocalModelServer, FirstRunState, GetAppExitWorkGuardState,
GetLiveStateLean, GetMemory, GetProjectPermissions, GetProjectSystemPermissions,
GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage,
GitStatus, GitUnstage, HarvestMemoryFromTurn, HealthUseCase, InspectConversation,
InstallPluginFromArchive, InstallPluginFromDirectory, JsonPluginManifestValidator, LaunchAgent,
LaunchAgentInput, LinkIssues, ListAgents, ListAgentsInput, ListClaudeModels, ListCodexModels,
ListDevices, ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories, ListModelServers,
ListOpenCodeProviders, ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects,
ListResumableAgents, ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions,
LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime,
McpToolPermissionCatalogue, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject,
OpenTerminal, OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice,
PermissionProjectorRegistry, ProposeContext, ReadAgentContext, ReadContext,
ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory,
ReadMemoryIndex, ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts,
@ -991,6 +991,8 @@ pub struct BackendCore {
pub bulk_update_issue_status: Arc<BulkUpdateIssueStatus>,
/// Update public ticket priorities in batch.
pub bulk_update_issue_priority: Arc<BulkUpdateIssuePriority>,
/// Delete public tickets in batch.
pub bulk_delete_issues: Arc<BulkDeleteIssues>,
/// Read a ticket carnet.
pub read_issue_carnet: Arc<ReadIssueCarnet>,
/// Update a ticket carnet.
@ -1684,6 +1686,10 @@ impl BackendCore {
Arc::clone(&clock) as Arc<dyn Clock>,
Arc::clone(&events_port),
));
let bulk_delete_issues = Arc::new(BulkDeleteIssues::new(
Arc::clone(&issue_store_port),
Arc::clone(&events_port),
));
let read_issue_carnet = Arc::new(ReadIssueCarnet::new(Arc::clone(&issue_store_port)));
let update_issue_carnet = Arc::new(UpdateIssueCarnet::new(
Arc::clone(&issue_store_port),
@ -2834,6 +2840,7 @@ impl BackendCore {
update_issue,
bulk_update_issue_status,
bulk_update_issue_priority,
bulk_delete_issues,
read_issue_carnet,
update_issue_carnet,
link_issues,
@ -4967,6 +4974,7 @@ mod mcp_serve_peer_tests {
"idea_ticket_update_priority",
"idea_ticket_bulk_update_status",
"idea_ticket_bulk_update_priority",
"idea_ticket_bulk_delete",
"idea_ticket_read_carnet",
"idea_ticket_update_carnet",
"idea_ticket_link",
@ -4987,8 +4995,8 @@ mod mcp_serve_peer_tests {
assert!(!names.contains(&"idea_reply"));
assert_eq!(
tools.len(),
33,
"exactly the thirty-three exposed idea_* tools; got {names:?}"
34,
"exactly the thirty-four exposed idea_* tools; got {names:?}"
);
drop(client); // EOF ⇒ serve loop ends

View File

@ -357,7 +357,9 @@ fn is_ticket_policy_mutation_tool(name: &str) -> bool {
fn is_ticket_policy_bulk_mutation_tool(name: &str) -> bool {
matches!(
name,
"idea_ticket_bulk_update_status" | "idea_ticket_bulk_update_priority"
"idea_ticket_bulk_update_status"
| "idea_ticket_bulk_update_priority"
| "idea_ticket_bulk_delete"
)
}

View File

@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid;
use application::{
BulkIssueMutationOutput, BulkUpdateIssuePriorityInput, BulkUpdateIssueStatusInput,
CreateIssueInput, UpdateIssueInput,
BulkDeleteIssuesInput, BulkIssueMutationOutput, BulkUpdateIssuePriorityInput,
BulkUpdateIssueStatusInput, CreateIssueInput, UpdateIssueInput,
};
use domain::{
AgentId, AgentIssueRole, Issue, IssueActor, IssueCarnet, IssueIndexEntry, IssueLink,
@ -225,6 +225,15 @@ pub struct TicketBulkUpdatePriorityRequestDto {
pub priority: String,
}
/// Bulk delete request.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TicketBulkDeleteRequestDto {
#[serde(default)]
pub project_id: String,
pub refs: Vec<String>,
}
/// Bulk operation response.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -757,6 +766,16 @@ pub fn bulk_update_priority_input(
})
}
pub fn bulk_delete_input(
project: Project,
request: TicketBulkDeleteRequestDto,
) -> Result<BulkDeleteIssuesInput, ErrorDto> {
Ok(BulkDeleteIssuesInput {
project,
issue_refs: parse_bulk_refs(request.refs)?,
})
}
impl From<BulkIssueMutationOutput> for TicketBulkResultDto {
fn from(output: BulkIssueMutationOutput) -> Self {
Self {
@ -1272,6 +1291,27 @@ mod tests {
assert!(err.message.contains("duplicate ticket ref: #1"));
}
#[test]
fn bulk_delete_input_maps_refs_in_order() {
let input = bulk_delete_input(
project_for_tests(),
TicketBulkDeleteRequestDto {
project_id: String::new(),
refs: vec!["#2".into(), "#1".into()],
},
)
.unwrap();
assert_eq!(
input
.issue_refs
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>(),
vec!["#2", "#1"]
);
}
#[test]
fn actor_from_requester_maps_agent_uuid_else_user() {
let agent_id = AgentId::new_random();

View File

@ -919,7 +919,9 @@ fn is_ticket_policy_mutation_tool(name: &str) -> bool {
fn is_ticket_policy_bulk_mutation_tool(name: &str) -> bool {
matches!(
name,
"idea_ticket_bulk_update_status" | "idea_ticket_bulk_update_priority"
"idea_ticket_bulk_update_status"
| "idea_ticket_bulk_update_priority"
| "idea_ticket_bulk_delete"
)
}

View File

@ -62,6 +62,7 @@ pub fn is_ticket_tool(name: &str) -> bool {
| "idea_ticket_update_priority"
| "idea_ticket_bulk_update_status"
| "idea_ticket_bulk_update_priority"
| "idea_ticket_bulk_delete"
| "idea_ticket_read_carnet"
| "idea_ticket_update_carnet"
| "idea_ticket_link"
@ -255,6 +256,18 @@ pub fn catalogue() -> Vec<ToolDef> {
"additionalProperties": false
}),
},
ToolDef {
name: "idea_ticket_bulk_delete",
description: "Delete several IdeA tickets and return one result per ticket.",
input_schema: json!({
"type": "object",
"properties": {
"refs": refs.clone()
},
"required": ["refs"],
"additionalProperties": false
}),
},
ToolDef {
name: "idea_ticket_read_carnet",
description: "Read the editable carnet attached to a ticket.",

View File

@ -76,6 +76,7 @@ pub const WRITE_ACTION_TOOLS: &[&str] = &[
"idea_ticket_update_priority",
"idea_ticket_bulk_update_status",
"idea_ticket_bulk_update_priority",
"idea_ticket_bulk_delete",
"idea_ticket_update_carnet",
"idea_ticket_link",
"idea_ticket_unlink",

View File

@ -751,6 +751,7 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
"idea_ticket_update_priority",
"idea_ticket_bulk_update_status",
"idea_ticket_bulk_update_priority",
"idea_ticket_bulk_delete",
"idea_ticket_read_carnet",
"idea_ticket_update_carnet",
"idea_ticket_link",
@ -771,8 +772,8 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
assert!(!names.contains(&"idea_reply"));
assert_eq!(
tools.len(),
33,
"exactly the thirty-three exposed idea_* tools; got {names:?}"
34,
"exactly the thirty-four exposed idea_* tools; got {names:?}"
);
// Every tool advertises an object input schema.
@ -1371,13 +1372,14 @@ async fn bounded_ticket_mutation_tools_reject_other_issue_before_ticket_provider
"idea_ticket_bulk_update_priority",
json!({ "refs": ["#8"], "priority": "high" }),
),
(25, "idea_ticket_bulk_delete", json!({ "refs": ["#8"] })),
(
25,
26,
"idea_ticket_link",
json!({ "ref": "#8", "targetRef": "#9", "kind": "blocks", "expectedVersion": 1 }),
),
(
26,
27,
"idea_ticket_unlink",
json!({ "ref": "#8", "targetRef": "#9", "kind": "blocks", "expectedVersion": 1 }),
),
@ -1442,13 +1444,14 @@ async fn bounded_ticket_mutation_tools_allow_bound_issue_to_reach_ticket_provide
"idea_ticket_bulk_update_priority",
json!({ "refs": ["#7"], "priority": "high" }),
),
(35, "idea_ticket_bulk_delete", json!({ "refs": ["#7"] })),
(
35,
36,
"idea_ticket_link",
json!({ "ref": "#7", "targetRef": "#9", "kind": "blocks", "expectedVersion": 1 }),
),
(
36,
37,
"idea_ticket_unlink",
json!({ "ref": "#7", "targetRef": "#9", "kind": "blocks", "expectedVersion": 1 }),
),