feat(domain,application,infrastructure,backend,app-tauri,web-server,frontend): restaurer les projets ouverts au redémarrage dans l'ordre mémorisé — #303 (état versionné OpenProjectRestoreState persisté sous <app_data_dir>/open-projects.json via port dédié OpenProjectRestoreStore, distinct du registre projects.json et sans recycler workspace.json; sept use cases cadrés: read/record opened/record closed/save order/restore/retry/remove, mutations sérialisées par mutex applicatif contre les lost updates, tombstones conservés à leur position avec lastFailure et codes stables notFound/inaccessible/store/invalid; prévalidation root par metadata+listing sans interrompre les autres restaurations; activation centralisée BackendCore::activate_project réutilisée par ouverture Tauri/HTTP, création, restore et retry: reconciliation layouts/live-state/background tasks, watcher orchestrateur + serveur MCP, réparation run dirs Claude; cinq commandes exposées Tauri et HTTP avec contrats DTO identiques: restore_open_projects, retry_open_project_restore, remove_project_from_restore, save_open_project_restore_order, read_open_project_restore_state; frontend: DTO de reprise + ProjectGateway étendu, useProjects initialise par listProjects+restoreOpenProjects et reconstruit openTabs dans l'ordre opened sans notification quand tout réussit, notification unique singulier/pluriel avec Voir les détails/Réessayer, détail par projet avec cause française et Retirer de la reprise, retry réussi réinséré à la position mémorisée, onglet conservé si closeProject échoué; saveOpenProjectRestoreOrder exposé non appelé car l'UI ne permet pas encore de réordonnancer; QA verte reconfirmée par Git: rustup run stable cargo fmt --all -- --check, git diff --check, cargo test -p backend open_project_restore_tests 1/1 couvrant absence initiale/ordre/fermeture/inaccessible/retry/notFound/remove/watcher+MCP, cargo test -p web-server authorized_open_project_restore_commands_share_the_tauri_contract 1/1, cargo test -p domain 311+ suites vertes, cargo check -p application -p infrastructure -p backend -p app-tauri -p web-server OK, npm run typecheck OK, npm test ciblé 3 fichiers/37 tests OK, npm run build OK; réserves hors périmètre documentées au carnet: application tests/layout_usecases.rs 2 échecs fixtures plugin package NotFound, infrastructure 1 timing test cancellation process, npm test large 32 échecs web shell/LS7/tickets web — préexistants sans lien avec #303; restant: rebuild AppImage demandé après validation puis validation live fermeture/relance; fichiers tickets/296/*, tickets/299-303/*, counter/index, attachments/ et scripts/ hors périmètre; aucun push)
This commit is contained in:
@ -53,17 +53,16 @@ use application::{
|
||||
PairAttemptDecision, PairDeviceInput, RateLimitKey, ReadAgentContextInput,
|
||||
ReadConversationPageInput, ReadIssueAttachmentInput, ReadIssueCarnetInput, ReadIssueInput,
|
||||
ReadMcpToolPermissionsInput, ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput,
|
||||
ReconcileLayoutsInput, ReconcileLiveStateInput, RenameDeviceInput, RenameLayoutInput,
|
||||
RenameSprintInput, ReorderSprintsInput, ResizeTerminalInput, ResolveAgentPermissionsInput,
|
||||
ResolveAgentSystemPermissionsInput, ResolveMemoryLinksInput, RevokeDeviceInput,
|
||||
RotateConversationLogInput, SetActiveLayoutInput, SetAgentMcpToolAllowedInput,
|
||||
StopLiveAgentInput, SyncAgentWithTemplateInput, TouchDeviceInput, UnassignSkillFromAgentInput,
|
||||
UnassignTicketFromSprintInput, UnlinkIssuesInput, UpdateAgentContactableInput,
|
||||
UpdateAgentContextInput, UpdateAgentEffortInput, UpdateAgentMcpToolPermissionsInput,
|
||||
UpdateAgentPermissionsInput, UpdateAgentSystemPermissionsInput, UpdateIssueCarnetInput,
|
||||
UpdateMemoryInput, UpdateProjectContextInput, UpdateProjectMcpToolPermissionsInput,
|
||||
UpdateProjectPermissionsInput, UpdateProjectSystemPermissionsInput, UpdateSkillInput,
|
||||
WriteToTerminalInput,
|
||||
RenameDeviceInput, RenameLayoutInput, RenameSprintInput, ReorderSprintsInput,
|
||||
ResizeTerminalInput, ResolveAgentPermissionsInput, ResolveAgentSystemPermissionsInput,
|
||||
ResolveMemoryLinksInput, RevokeDeviceInput, RotateConversationLogInput, SetActiveLayoutInput,
|
||||
SetAgentMcpToolAllowedInput, StopLiveAgentInput, SyncAgentWithTemplateInput, TouchDeviceInput,
|
||||
UnassignSkillFromAgentInput, UnassignTicketFromSprintInput, UnlinkIssuesInput,
|
||||
UpdateAgentContactableInput, UpdateAgentContextInput, UpdateAgentEffortInput,
|
||||
UpdateAgentMcpToolPermissionsInput, UpdateAgentPermissionsInput,
|
||||
UpdateAgentSystemPermissionsInput, UpdateIssueCarnetInput, UpdateMemoryInput,
|
||||
UpdateProjectContextInput, UpdateProjectMcpToolPermissionsInput, UpdateProjectPermissionsInput,
|
||||
UpdateProjectSystemPermissionsInput, UpdateSkillInput, WriteToTerminalInput,
|
||||
};
|
||||
use domain::ports::{
|
||||
PtyHandle, ReplyEvent, ReplyProgress, ReplyProgressKind, ReplyProgressSource,
|
||||
@ -92,19 +91,21 @@ use backend::dto::{
|
||||
GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto,
|
||||
LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto, LiveAgentListDto,
|
||||
MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto, OpenCodeProviderListDto,
|
||||
OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProfileModelCatalogDto, ProjectDto,
|
||||
ProjectListDto, ProjectMcpToolPermissionsDto, ProjectPermissionsDto,
|
||||
ProjectSystemPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto,
|
||||
ReadConversationPageRequestDto, RecallMemoryRequestDto, RenameLayoutRequestDto,
|
||||
OpenProjectRestoreStateDto, OpenTerminalRequestDto, ProfileDto, ProfileListDto,
|
||||
ProfileModelCatalogDto, ProjectDto, ProjectListDto, ProjectMcpToolPermissionsDto,
|
||||
ProjectPermissionsDto, ProjectSystemPermissionsDto, ProjectWorkStateDto,
|
||||
ReadAgentContextResponseDto, ReadConversationPageRequestDto, RecallMemoryRequestDto,
|
||||
RemoveOpenProjectRestoreEntryRequestDto, RenameLayoutRequestDto,
|
||||
ResolveAgentPermissionsRequestDto, ResolveAgentPermissionsResponseDto,
|
||||
ResolveAgentSystemPermissionsRequestDto, ResolvedAgentSystemPermissionsDto,
|
||||
ResumableAgentListDto, SaveEmbedderProfileRequestDto, SaveOpenCodeProviderProfileRequestDto,
|
||||
SaveProfileRequestDto, SetActiveLayoutRequestDto, SetActiveLayoutResultDto,
|
||||
SetAgentMcpToolAllowedRequestDto, SkillDto, SkillListDto, SprintCreateRequestDto,
|
||||
SprintDeleteRequestDto, SprintDto, SprintListDto, SprintListRequestDto, SprintRenameRequestDto,
|
||||
SprintReorderRequestDto, StopLiveAgentRequestDto, StopLiveAgentResponseDto,
|
||||
SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
|
||||
TerminalSessionDto, TicketAssignRequestDto, TicketAttachmentAddRequestDto,
|
||||
RestoreOpenProjectsOutputDto, ResumableAgentListDto, RetryOpenProjectRestoreRequestDto,
|
||||
SaveEmbedderProfileRequestDto, SaveOpenCodeProviderProfileRequestDto,
|
||||
SaveOpenProjectRestoreOrderRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto,
|
||||
SetActiveLayoutResultDto, SetAgentMcpToolAllowedRequestDto, SkillDto, SkillListDto,
|
||||
SprintCreateRequestDto, SprintDeleteRequestDto, SprintDto, SprintListDto, SprintListRequestDto,
|
||||
SprintRenameRequestDto, SprintReorderRequestDto, StopLiveAgentRequestDto,
|
||||
StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
|
||||
TemplateListDto, TerminalSessionDto, TicketAssignRequestDto, TicketAttachmentAddRequestDto,
|
||||
TicketAttachmentContentDto, TicketAttachmentMarkSummarizedRequestDto,
|
||||
TicketAttachmentReadRequestDto, TicketCarnetDto, TicketCreateRequestDto,
|
||||
TicketDeleteRequestDto, TicketDto, TicketLinkCommandRequestDto, TicketListPageInput,
|
||||
@ -3346,6 +3347,19 @@ async fn invoke(
|
||||
"health" => invoke_health(&request.args, &state.app),
|
||||
"list_projects" => invoke_list_projects(&state.app).await,
|
||||
"open_project" => invoke_open_project(&request.args, &state.app).await,
|
||||
"read_open_project_restore_state" => {
|
||||
invoke_read_open_project_restore_state(&state.app).await
|
||||
}
|
||||
"restore_open_projects" => invoke_restore_open_projects(&state.app).await,
|
||||
"retry_open_project_restore" => {
|
||||
invoke_retry_open_project_restore(&request.args, &state.app).await
|
||||
}
|
||||
"remove_project_from_restore" => {
|
||||
invoke_remove_project_from_restore(&request.args, &state.app).await
|
||||
}
|
||||
"save_open_project_restore_order" => {
|
||||
invoke_save_open_project_restore_order(&request.args, &state.app).await
|
||||
}
|
||||
"load_layout" => invoke_load_layout(&request.args, &state.app).await,
|
||||
"mutate_layout" => invoke_mutate_layout(&request.args, &state.app).await,
|
||||
"list_layouts" => invoke_list_layouts(&request.args, &state.app).await,
|
||||
@ -3544,24 +3558,89 @@ async fn open_project_for_web(
|
||||
) -> Result<ProjectDto, ErrorDto> {
|
||||
let id = parse_project_id(project_id)?;
|
||||
let output = state
|
||||
.open_project
|
||||
.execute(OpenProjectInput { project_id: id })
|
||||
.open_project_and_activate(id)
|
||||
.await
|
||||
.map_err(ErrorDto::from)?;
|
||||
let _ = state
|
||||
.reconcile_layouts
|
||||
.execute(ReconcileLayoutsInput { project_id: id })
|
||||
.await;
|
||||
let _ = state
|
||||
.reconcile_live_state
|
||||
.execute(ReconcileLiveStateInput { project_id: id })
|
||||
.await;
|
||||
let _ = state.reconcile_background_tasks.execute(id).await;
|
||||
state.ensure_orchestrator_watch(&output.project);
|
||||
state.reconcile_claude_run_dirs(&output.project).await;
|
||||
Ok(ProjectDto::from(output))
|
||||
}
|
||||
|
||||
async fn invoke_read_open_project_restore_state(state: &BackendCore) -> Result<Value, ErrorDto> {
|
||||
let output = state
|
||||
.read_open_project_restore_state
|
||||
.execute()
|
||||
.await
|
||||
.map(OpenProjectRestoreStateDto::from)
|
||||
.map_err(ErrorDto::from)?;
|
||||
serde_json::to_value(output).map_err(serialization_error)
|
||||
}
|
||||
|
||||
async fn invoke_restore_open_projects(state: &BackendCore) -> Result<Value, ErrorDto> {
|
||||
let output = state
|
||||
.restore_open_projects_and_activate()
|
||||
.await
|
||||
.map(RestoreOpenProjectsOutputDto::from)
|
||||
.map_err(ErrorDto::from)?;
|
||||
serde_json::to_value(output).map_err(serialization_error)
|
||||
}
|
||||
|
||||
async fn invoke_retry_open_project_restore(
|
||||
args: &Value,
|
||||
state: &BackendCore,
|
||||
) -> Result<Value, ErrorDto> {
|
||||
let request = optional_request::<RetryOpenProjectRestoreRequestDto>(args)?.unwrap_or_default();
|
||||
let project_id = request
|
||||
.project_id
|
||||
.as_deref()
|
||||
.map(parse_project_id)
|
||||
.transpose()?;
|
||||
let output = state
|
||||
.retry_open_project_restore_and_activate(project_id)
|
||||
.await
|
||||
.map(RestoreOpenProjectsOutputDto::from)
|
||||
.map_err(ErrorDto::from)?;
|
||||
serde_json::to_value(output).map_err(serialization_error)
|
||||
}
|
||||
|
||||
async fn invoke_remove_project_from_restore(
|
||||
args: &Value,
|
||||
state: &BackendCore,
|
||||
) -> Result<Value, ErrorDto> {
|
||||
let request = required_request::<RemoveOpenProjectRestoreEntryRequestDto>(
|
||||
"remove_project_from_restore",
|
||||
args,
|
||||
)?;
|
||||
let project_id = parse_project_id(&request.project_id)?;
|
||||
let output = state
|
||||
.remove_project_from_restore
|
||||
.execute(project_id)
|
||||
.await
|
||||
.map(OpenProjectRestoreStateDto::from)
|
||||
.map_err(ErrorDto::from)?;
|
||||
serde_json::to_value(output).map_err(serialization_error)
|
||||
}
|
||||
|
||||
async fn invoke_save_open_project_restore_order(
|
||||
args: &Value,
|
||||
state: &BackendCore,
|
||||
) -> Result<Value, ErrorDto> {
|
||||
let request = required_request::<SaveOpenProjectRestoreOrderRequestDto>(
|
||||
"save_open_project_restore_order",
|
||||
args,
|
||||
)?;
|
||||
let ordered_project_ids = request
|
||||
.ordered_project_ids
|
||||
.iter()
|
||||
.map(|id| parse_project_id(id))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let output = state
|
||||
.save_open_project_restore_order
|
||||
.execute(ordered_project_ids)
|
||||
.await
|
||||
.map(OpenProjectRestoreStateDto::from)
|
||||
.map_err(ErrorDto::from)?;
|
||||
serde_json::to_value(output).map_err(serialization_error)
|
||||
}
|
||||
|
||||
async fn invoke_load_layout(args: &Value, state: &BackendCore) -> Result<Value, ErrorDto> {
|
||||
let request = required_request::<LoadLayoutInvokeArgs>("load_layout", args)?;
|
||||
let project_id = parse_project_id(&request.project_id)?;
|
||||
@ -9538,6 +9617,51 @@ mod tests {
|
||||
.is_some_and(|root| root.contains("idea-server-project-")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authorized_open_project_restore_commands_share_the_tauri_contract() {
|
||||
let state = state();
|
||||
let project_id = create_project_for_test(&state, "Restore Web").await;
|
||||
let cookie = pair_and_cookie(Arc::clone(&state)).await;
|
||||
|
||||
let opened = request(
|
||||
Arc::clone(&state),
|
||||
Method::POST,
|
||||
"/api/invoke",
|
||||
json!({ "command": "open_project", "args": { "projectId": project_id } }),
|
||||
&[("cookie", &cookie)],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response_json(opened).await.0, StatusCode::OK);
|
||||
|
||||
let restored = request(
|
||||
Arc::clone(&state),
|
||||
Method::POST,
|
||||
"/api/invoke",
|
||||
json!({ "command": "restore_open_projects", "args": {} }),
|
||||
&[("cookie", &cookie)],
|
||||
)
|
||||
.await;
|
||||
let (status, body, _) = response_json(restored).await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(body["opened"][0]["id"], project_id);
|
||||
assert_eq!(body["failures"], json!([]));
|
||||
|
||||
let removed = request(
|
||||
state,
|
||||
Method::POST,
|
||||
"/api/invoke",
|
||||
json!({
|
||||
"command": "remove_project_from_restore",
|
||||
"args": { "request": { "projectId": project_id } }
|
||||
}),
|
||||
&[("cookie", &cookie)],
|
||||
)
|
||||
.await;
|
||||
let (status, body, _) = response_json(removed).await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(body["entries"], json!([]));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authorized_open_project_returns_readonly_project_dto() {
|
||||
let state = state();
|
||||
|
||||
Reference in New Issue
Block a user