feat(backend,domain,infrastructure,application,app-tauri,web-server,frontend): suppression d'un projet du registre IdeA sans toucher son dossier disque — #308 (QA verte)

cause: l'utilisateur ne pouvait pas retirer un projet du registre IdeA (projects.json) ; la seule issue laissait l'entrée fantôme dans la liste des projets, sans capacité générique de retrait.

domain: port ProjectStore::remove_project(id) -> Option<Project> avec implémentation par défaut « not supported » pour ne pas casser les stores spécialisés.

infrastructure: FsProjectStore implémente remove_project — retire l'entrée du registre persisté et la renvoie, None si id inconnu, ordre relatif du registre préservé ; test store dédié.

application: use case RemoveProjectFromRegistry (remove.rs) — NotFound explicite si id absent, retourne le projet retiré + registre rafraîchi.

backend/app-tauri/web-server: DTO RemoveProjectRequestDto/RemoveProjectResponseDto (removed, projects, restore_state, was_open), adapter partagé remove_project_for_adapter réutilisé par la commande Tauri remove_project et la route HTTP — même contrat desktop/web ; remove_project_and_deactivate désactive le projet s'il était ouvert, purge l'entrée open-projects restore correspondante et renvoie was_open/restore_state ; test adapter vérifiant le contrat de bout en bout (entrée retirée, restore purgé, root intact).

frontend: DTO + ProjectGateway.removeProject (HTTP + mock), ProjectsView expose un menu d'actions projet (ProjectActionsMenu) avec entrée Retirer, RemoveProjectDialog — confirmation explicite précisant que le dossier disque n'est pas supprimé, invalidation/rechargement de la liste des projets après succès (useProjects), tests ProjectsView/projects/adapters/httpInvoker/services.

QA verte: cargo targeted tests/check OK (domain, application project_usecases, infrastructure project_store, backend, app-tauri, web-server), npm targeted/typecheck/build OK ; suite npm test complète rouge non bloquante hors #308 (échecs préexistants listProjectSettings/web). Vérifications Git: git diff --check OK, cargo fmt --all -- --check OK.

fichiers .ideai/tickets/308/, tickets/counter.json, tickets/index.json, attachments/ et scripts/ hors périmètre et préservés en worktree (sync store traité séparément) ; aucun push.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 18:27:15 +02:00
parent ca1a27a2b9
commit b9f8ed303e
25 changed files with 1009 additions and 29 deletions

View File

@ -69,15 +69,16 @@ use crate::dto::{
ProjectListDto, ProjectMcpToolPermissionsDto, ProjectPermissionsDto,
ProjectSystemPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto,
ReadChatDraftResponseDto, ReadConversationPageRequestDto, ReattachChatDto, ReattachResultDto,
RecallMemoryRequestDto, RemoveOpenProjectRestoreEntryRequestDto, RenameLayoutRequestDto,
ReplyChunk, ResizeTerminalRequestDto, ResolveAgentPermissionsRequestDto,
ResolveAgentPermissionsResponseDto, ResolveAgentSystemPermissionsRequestDto,
ResolvedAgentSystemPermissionsDto, RestoreOpenProjectsOutputDto, ResumableAgentListDto,
RetryOpenProjectRestoreRequestDto, SaveChatDraftRequestDto, SaveEmbedderProfileRequestDto,
SaveModelServerRequestDto, SaveOpenCodeProviderProfileRequestDto,
SaveOpenProjectRestoreOrderRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto,
SetActiveLayoutResultDto, SetAgentMcpToolAllowedRequestDto, SkillDto, SkillListDto,
SlashCommandEffectDto, SlashCommandListDto, StopLiveAgentRequestDto, StopLiveAgentResponseDto,
RecallMemoryRequestDto, RemoveOpenProjectRestoreEntryRequestDto, RemoveProjectRequestDto,
RemoveProjectResponseDto, RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto,
ResolveAgentPermissionsRequestDto, ResolveAgentPermissionsResponseDto,
ResolveAgentSystemPermissionsRequestDto, ResolvedAgentSystemPermissionsDto,
RestoreOpenProjectsOutputDto, ResumableAgentListDto, RetryOpenProjectRestoreRequestDto,
SaveChatDraftRequestDto, SaveEmbedderProfileRequestDto, SaveModelServerRequestDto,
SaveOpenCodeProviderProfileRequestDto, SaveOpenProjectRestoreOrderRequestDto,
SaveProfileRequestDto, SetActiveLayoutRequestDto, SetActiveLayoutResultDto,
SetAgentMcpToolAllowedRequestDto, SkillDto, SkillListDto, SlashCommandEffectDto,
SlashCommandListDto, StopLiveAgentRequestDto, StopLiveAgentResponseDto,
SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
TerminalClosedDto, TerminalSessionDto, TurnPageDto, UnassignSkillRequestDto,
UpdateAgentContactableRequestDto, UpdateAgentContextRequestDto, UpdateAgentEffortRequestDto,
@ -584,6 +585,45 @@ mod open_project_template_tests {
assert!(after_open.default_system_permissions.is_none());
assert!(after_open.default_mcp_tool_policy.is_none());
}
#[tokio::test]
async fn remove_project_adapter_returns_the_shared_contract() {
let state = AppState::build(temp_path("remove-appdata"));
let project_root = temp_path("remove-project");
let project = state
.create_project
.execute(CreateProjectInput {
name: "Remove Adapter".to_owned(),
root: project_root.to_string_lossy().into_owned(),
remote: None,
default_profile_id: None,
})
.await
.unwrap()
.project;
state
.record_project_opened_for_restore
.execute(project.clone())
.await
.unwrap();
let response = remove_project_for_adapter(
RemoveProjectRequestDto {
project_id: project.id.to_string(),
},
&state,
)
.await
.unwrap();
assert_eq!(response.removed.id, project.id.to_string());
assert!(response.projects.is_empty());
assert!(response.restore_state.entries.is_empty());
assert!(response.was_open);
assert!(project_root.exists());
let _ = std::fs::remove_dir_all(project_root);
}
}
/// `close_project` — persist state and release resources for a project.
@ -619,6 +659,28 @@ pub async fn close_project(project_id: String, state: State<'_, AppState>) -> Re
.map_err(ErrorDto::from)
}
/// Removes one project from IdeA's registry without touching its root directory.
#[tauri::command]
pub async fn remove_project(
request: RemoveProjectRequestDto,
state: State<'_, AppState>,
) -> Result<RemoveProjectResponseDto, ErrorDto> {
remove_project_for_adapter(request, &state).await
}
/// Shared desktop/web adapter body for project removal.
pub async fn remove_project_for_adapter(
request: RemoveProjectRequestDto,
state: &AppState,
) -> Result<RemoveProjectResponseDto, ErrorDto> {
let project_id = parse_project_id(&request.project_id)?;
state
.remove_project_and_deactivate(project_id)
.await
.map(Into::into)
.map_err(ErrorDto::from)
}
/// Reads the ordered open-project restore state without opening projects.
#[tauri::command]
pub async fn read_open_project_restore_state(

View File

@ -254,6 +254,7 @@ pub fn run() {
commands::create_project,
commands::open_project,
commands::close_project,
commands::remove_project,
commands::read_open_project_restore_state,
commands::restore_open_projects,
commands::retry_open_project_restore,