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:
2026-07-06 06:27:09 +02:00
parent d25e340b44
commit 5417bd756b
20 changed files with 689 additions and 95 deletions

View File

@ -241,6 +241,16 @@ pub enum DomainEventDto {
/// New optimistic version.
version: u64,
},
/// An issue-backed public ticket was deleted.
#[serde(rename_all = "camelCase")]
IssueDeleted {
/// Project id.
project_id: String,
/// Deleted public ticket reference (`#N`).
issue_ref: String,
/// Released sprint id, when any.
freed_sprint: Option<String>,
},
/// A public ticket status changed.
#[serde(rename_all = "camelCase")]
IssueStatusChanged {
@ -781,6 +791,15 @@ impl From<&DomainEvent> for DomainEventDto {
issue_ref: issue_ref.to_string(),
version: version.get(),
},
DomainEvent::IssueDeleted {
project_id,
issue_ref,
freed_sprint,
} => Self::IssueDeleted {
project_id: project_id.to_string(),
issue_ref: issue_ref.to_string(),
freed_sprint: freed_sprint.map(|sprint| sprint.to_string()),
},
DomainEvent::IssueStatusChanged {
issue_ref,
status,
@ -1106,6 +1125,22 @@ mod tests {
assert_eq!(json["version"], 3);
}
#[test]
fn issue_deleted_relays_to_dto_and_wire() {
let project_id = domain::ProjectId::from_uuid(uuid::Uuid::from_u128(40));
let sprint_id = domain::SprintId::from_uuid(uuid::Uuid::from_u128(42));
let dto = DomainEventDto::from(&DomainEvent::IssueDeleted {
project_id,
issue_ref: domain::IssueRef::from(domain::IssueNumber::new(7).unwrap()),
freed_sprint: Some(sprint_id),
});
let json = serde_json::to_value(&dto).expect("serialisable");
assert_eq!(json["type"], "issueDeleted");
assert_eq!(json["projectId"], project_id.to_string());
assert_eq!(json["issueRef"], "#7");
assert_eq!(json["freedSprint"], sprint_id.to_string());
}
#[test]
fn ticket_assistant_events_relay_to_dto_and_wire() {
let profile_id = domain::ProfileId::from_uuid(uuid::Uuid::from_u128(9));

View File

@ -166,6 +166,7 @@ pub fn run() {
commands::list_agents,
tickets::ticket_create,
tickets::ticket_read,
tickets::ticket_delete,
tickets::open_ticket_chat,
tickets::close_ticket_chat,
tickets::ticket_list,

View File

@ -17,28 +17,29 @@ use application::{
ChangeAgentProfile, CheckEmbedderSuggestion, CloseProject, CloseTab, CloseTerminal,
CloseTicketAssistant, ConfigureProfiles, ContextGuardUseCases, CreateAgentFromScratch,
CreateAgentFromTemplate, CreateIssue, CreateLayout, CreateMemory, CreateProject, CreateSkill,
CreateSprint, CreateTemplate, DeleteAgent, DeleteEmbedderProfile, DeleteLayout, DeleteMemory,
DeleteProfile, DeleteSkill, DeleteSprint, DeleteTemplate, DescribeEmbedderEngines,
DetectAgentDrift, DetectProfiles, DismissEmbedderSuggestion, FirstRunState, GetLiveStateLean,
GetMemory, GetProjectPermissions, GetProjectWorkState, GitBranches, GitCheckout, GitCommit,
GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage, HarvestMemoryFromTurn,
HealthUseCase, InspectConversation, LaunchAgent, LaunchAgentInput, LinkIssues, ListAgents,
ListAgentsInput, ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories, ListProfiles,
ListProjects, ListResumableAgents, ListSkills, ListSprints, ListTemplates, LiveAgentRegistry,
LiveSessions, LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout,
McpRuntime, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
OpenTicketAssistant, OrchestratorService, PermissionProjectorRegistry, ProposeContext,
ReadAgentContext, ReadContext, ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMemory,
ReadMemoryIndex, ReadProjectContext, ReadSkill, RecallMemory, ReconcileLayouts,
ReconcileLiveState, ReconcileLiveStateInput, RecordTurn, RecordTurnProvider, ReferenceProfiles,
RenameLayout, RenameSprint, ReorderSprints, ResizeTerminal, ResolveAgentPermissions,
ResolveMemoryLinks, RetryBackgroundTask, RotateConversationLog, SaveEmbedderProfile,
SaveProfile, SessionLimitService, SetActiveLayout, SnapshotRunningAgents,
SpawnBackgroundCommand, StopLiveAgent, StructuredSessions, SuggestedThisSession,
SyncAgentWithTemplate, TerminalSessions, UnassignSkillFromAgent, UnassignTicketFromSprint,
UnlinkIssues, UpdateAgentContext, UpdateAgentPermissions, UpdateIssue, UpdateIssueCarnet,
UpdateLiveState, UpdateMemory, UpdateProjectContext, UpdateProjectPermissions, UpdateSkill,
UpdateTemplate, WakeSessionProvider, WriteMemory, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
CreateSprint, CreateTemplate, DeleteAgent, DeleteEmbedderProfile, DeleteIssue, DeleteLayout,
DeleteMemory, DeleteProfile, DeleteSkill, DeleteSprint, DeleteTemplate,
DescribeEmbedderEngines, DetectAgentDrift, DetectProfiles, DismissEmbedderSuggestion,
FirstRunState, GetLiveStateLean, GetMemory, GetProjectPermissions, GetProjectWorkState,
GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus,
GitUnstage, HarvestMemoryFromTurn, HealthUseCase, InspectConversation, LaunchAgent,
LaunchAgentInput, LinkIssues, ListAgents, ListAgentsInput, ListEmbedderProfiles, ListIssues,
ListLayouts, ListMemories, ListProfiles, ListProjects, ListResumableAgents, ListSkills,
ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions, LiveStateLeanProvider,
LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime, MoveTabToNewWindow,
MutateLayout, OnnxModelView, OpenProject, OpenTerminal, OpenTicketAssistant,
OrchestratorService, PermissionProjectorRegistry, ProposeContext, ReadAgentContext,
ReadContext, ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMemory, ReadMemoryIndex,
ReadProjectContext, ReadSkill, RecallMemory, ReconcileLayouts, ReconcileLiveState,
ReconcileLiveStateInput, RecordTurn, RecordTurnProvider, ReferenceProfiles, RenameLayout,
RenameSprint, ReorderSprints, ResizeTerminal, ResolveAgentPermissions, ResolveMemoryLinks,
RetryBackgroundTask, RotateConversationLog, SaveEmbedderProfile, SaveProfile,
SessionLimitService, SetActiveLayout, SnapshotRunningAgents, SpawnBackgroundCommand,
StopLiveAgent, StructuredSessions, SuggestedThisSession, SyncAgentWithTemplate,
TerminalSessions, UnassignSkillFromAgent, UnassignTicketFromSprint, UnlinkIssues,
UpdateAgentContext, UpdateAgentPermissions, UpdateIssue, UpdateIssueCarnet, UpdateLiveState,
UpdateMemory, UpdateProjectContext, UpdateProjectPermissions, UpdateSkill, UpdateTemplate,
WakeSessionProvider, WriteMemory, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
};
use async_trait::async_trait;
use domain::ports::{
@ -793,6 +794,8 @@ pub struct AppState {
pub create_issue: Arc<CreateIssue>,
/// Read an issue-backed public ticket.
pub read_issue: Arc<ReadIssue>,
/// Delete an issue-backed public ticket.
pub delete_issue: Arc<DeleteIssue>,
/// List issue-backed public tickets.
pub list_issues: Arc<ListIssues>,
/// Update an issue-backed public ticket.
@ -1207,6 +1210,10 @@ impl AppState {
Arc::clone(&events_port),
));
let read_issue = Arc::new(ReadIssue::new(Arc::clone(&issue_store_port)));
let delete_issue = Arc::new(DeleteIssue::new(
Arc::clone(&issue_store_port),
Arc::clone(&events_port),
));
let list_issues = Arc::new(ListIssues::new(Arc::clone(&issue_store_port)));
let update_issue = Arc::new(UpdateIssue::new(
Arc::clone(&issue_store_port),
@ -2190,6 +2197,7 @@ impl AppState {
event_bus,
create_issue,
read_issue,
delete_issue,
list_issues,
update_issue,
read_issue_carnet,

View File

@ -15,10 +15,10 @@ use uuid::Uuid;
use application::{
AppError, AssignIssueAgentInput, AssignTicketToSprintInput, CloseTicketAssistantInput,
CreateIssueInput, CreateSprintInput, DeleteSprintInput, LinkIssuesInput, ListIssuesInput,
ListSprintsInput, OpenProjectInput, OpenTicketAssistantInput, ReadIssueCarnetInput,
ReadIssueInput, RenameSprintInput, ReorderSprintsInput, UnassignTicketFromSprintInput,
UnlinkIssuesInput, UpdateIssueCarnetInput, UpdateIssueInput,
CreateIssueInput, CreateSprintInput, DeleteIssueInput, DeleteSprintInput, LinkIssuesInput,
ListIssuesInput, ListSprintsInput, OpenProjectInput, OpenTicketAssistantInput,
ReadIssueCarnetInput, ReadIssueInput, RenameSprintInput, ReorderSprintsInput,
UnassignTicketFromSprintInput, UnlinkIssuesInput, UpdateIssueCarnetInput, UpdateIssueInput,
};
use domain::{
AgentId, AgentIssueRole, Issue, IssueActor, IssueCarnet, IssueIndexEntry, IssueLink,
@ -180,6 +180,15 @@ pub struct TicketReadRequestDto {
pub include_carnet: Option<bool>,
}
/// Delete request.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TicketDeleteRequestDto {
#[serde(default)]
pub project_id: String,
pub r#ref: String,
}
/// List request.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -612,6 +621,23 @@ pub async fn ticket_read(
Ok(TicketDto::from_issue(issue, carnet))
}
#[tauri::command]
pub async fn ticket_delete(
request: TicketDeleteRequestDto,
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
state
.delete_issue
.execute(DeleteIssueInput {
project,
issue_ref: parse_ref_dto(&request.r#ref)?,
})
.await
.map(|_| ())
.map_err(ErrorDto::from)
}
#[tauri::command]
pub async fn open_ticket_chat(
request: OpenTicketChatRequestDto,