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

@ -133,6 +133,20 @@ impl ProjectStore for FsProjectStore {
self.write_registry(&registry).await
}
async fn remove_project(&self, id: ProjectId) -> Result<Option<Project>, StoreError> {
let mut registry = self.read_registry().await?;
let Some(position) = registry
.projects
.iter()
.position(|project| project.id == id)
else {
return Ok(None);
};
let removed = registry.projects.remove(position);
self.write_registry(&registry).await?;
Ok(Some(removed))
}
async fn save_workspace(&self, workspace: &Workspace) -> Result<(), StoreError> {
self.ensure_dir().await?;
let bytes = serde_json::to_vec_pretty(workspace)

View File

@ -92,6 +92,44 @@ async fn missing_registry_lists_empty() {
assert!(listed.is_empty());
}
#[tokio::test]
async fn remove_project_preserves_order_and_never_touches_project_root() {
let tmp = TempDir::new();
let store = store(&tmp);
let root = tmp.0.join("owned-project-root");
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("keep.txt"), b"untouched").unwrap();
let first = sample_project(ProjectId::new_random(), "first", "/outside/first");
let removed = sample_project(ProjectId::new_random(), "removed", &root.to_string_lossy());
let last = sample_project(ProjectId::new_random(), "last", "/outside/last");
for project in [&first, &removed, &last] {
store.save_project(project).await.unwrap();
}
assert_eq!(
store.remove_project(removed.id).await.unwrap(),
Some(removed)
);
assert_eq!(store.list_projects().await.unwrap(), vec![first, last]);
assert_eq!(std::fs::read(root.join("keep.txt")).unwrap(), b"untouched");
}
#[tokio::test]
async fn remove_missing_project_does_not_rewrite_the_registry() {
let tmp = TempDir::new();
let store = store(&tmp);
let project = sample_project(ProjectId::new_random(), "kept", "/outside/kept");
store.save_project(&project).await.unwrap();
let before = std::fs::read(tmp.0.join("projects.json")).unwrap();
assert_eq!(
store.remove_project(ProjectId::new_random()).await.unwrap(),
None
);
assert_eq!(std::fs::read(tmp.0.join("projects.json")).unwrap(), before);
}
#[tokio::test]
async fn workspace_save_then_load_roundtrips() {
let tmp = TempDir::new();