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:
@ -44,14 +44,16 @@ use application::{
|
||||
PluginToolchainDiagnostics, PluginWorkspaceAccess, ProjectPluginResolver, ProposeContext,
|
||||
PurgeManagedTemp, QueryProjectStructure, ReadAgentContext, ReadChatDraft, ReadContext,
|
||||
ReadConversationPage, ReadIssue, ReadIssueAttachment, ReadIssueCarnet, ReadMcpToolPermissions,
|
||||
ReadMemory, ReadMemoryIndex, ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory,
|
||||
ReconcileLayouts, ReconcileLiveState, ReconcileLiveStateInput, ReconcilePluginMcpServers,
|
||||
ReadMemory, ReadMemoryIndex, ReadOpenProjectRestoreState, ReadProjectContext, ReadSkill,
|
||||
ReadTemplate, RecallMemory, ReconcileLayouts, ReconcileLiveState, ReconcileLiveStateInput,
|
||||
ReconcilePluginMcpServers, RecordProjectClosedForRestore, RecordProjectOpenedForRestore,
|
||||
RecordTurn, RecordTurnProvider, ReferenceProfiles, ReleaseManagedTemp, ReloadPlugin,
|
||||
ReloadPluginInput, RenameDevice, RenameLayout, RenameSprint, ReorderSprints, ResizeTerminal,
|
||||
ResolveAgentCapabilities, ResolveAgentPermissions, ResolveAgentSystemPermissions,
|
||||
ResolveManagedTemp, ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask,
|
||||
ReviewPluginPackage, RevokeAllDevices, RevokeDevice, RotateConversationLog, SaveChatDraft,
|
||||
SaveEmbedderProfile, SaveModelServer, SaveOpenCodeProviderProfile, SaveProfile,
|
||||
ResolveManagedTemp, ResolveMemoryLinks, RestoreOpenProjects, RestoreOpenProjectsOutput,
|
||||
RestoreOpenWindows, RetryBackgroundTask, RetryOpenProjectRestore, ReviewPluginPackage,
|
||||
RevokeAllDevices, RevokeDevice, RotateConversationLog, SaveChatDraft, SaveEmbedderProfile,
|
||||
SaveModelServer, SaveOpenCodeProviderProfile, SaveOpenProjectRestoreOrder, SaveProfile,
|
||||
SessionLimitService, SetActiveLayout, SetAgentMcpToolAllowed, SetPluginEnabled,
|
||||
SetProjectPluginEnabled, SlashCommandRegistry, SnapshotOpenWindows, SnapshotRunningAgents,
|
||||
SpawnBackgroundCommand, StopLiveAgent, StructuredRoutingMode, StructuredSessions,
|
||||
@ -72,12 +74,13 @@ use domain::ports::{
|
||||
BackgroundTaskStore, ChatAttachmentStore, ChatDraftStore, Clock, DeviceSessionStore, Embedder,
|
||||
EmbedderEnvInspector, EmbedderProfileStore, EmbedderPromptStore, EnvironmentReader, EventBus,
|
||||
FileSystem, GitPort, IdGenerator, IssueNumberAllocator, IssueStore, ManagedTempStore,
|
||||
McpToolPermissionStore, MemoryRecall, MemoryStore, ModelArtifactDownloader, PermissionStore,
|
||||
PluginManifestValidator, PluginMcpSupervisor, PluginPackageStore, PluginRegistryStore,
|
||||
PluginStorageStore, ProcessSpawner, ProfileStore, ProjectPluginSettingsStore, ProjectStore,
|
||||
PtyHandle, PtyPort, RuntimePermissionProbe, ScheduledTask, Scheduler, SecretStore, SkillStore,
|
||||
SprintStore, StructuredSessionEnvironmentPreparer, SystemPermissionStore, TemplateStore,
|
||||
ToolInvoker, WakeError, WakeReason, WindowStateStore,
|
||||
McpToolPermissionStore, MemoryRecall, MemoryStore, ModelArtifactDownloader,
|
||||
OpenProjectRestoreStore, PermissionStore, PluginManifestValidator, PluginMcpSupervisor,
|
||||
PluginPackageStore, PluginRegistryStore, PluginStorageStore, ProcessSpawner, ProfileStore,
|
||||
ProjectPluginSettingsStore, ProjectStore, PtyHandle, PtyPort, RuntimePermissionProbe,
|
||||
ScheduledTask, Scheduler, SecretStore, SkillStore, SprintStore,
|
||||
StructuredSessionEnvironmentPreparer, SystemPermissionStore, TemplateStore, ToolInvoker,
|
||||
WakeError, WakeReason, WindowStateStore,
|
||||
};
|
||||
use domain::profile::{
|
||||
AgentProfile, ContextInjection, McpConfigStrategy, McpTransport, StructuredAdapter,
|
||||
@ -100,20 +103,21 @@ use infrastructure::{
|
||||
FsAssistantContextStore, FsBackgroundTaskStore, FsChatAttachmentStore, FsChatDraftStore,
|
||||
FsConversationLog, FsDeviceSessionStore, FsEmbedderProfileStore, FsEmbedderPromptStore,
|
||||
FsHandoffStore, FsIssueNumberAllocator, FsIssueStore, FsLiveStateStore, FsManagedTempStore,
|
||||
FsMcpToolPermissionStore, FsMemoryStore, FsModelServerRegistry, FsOrchestratorWatcher,
|
||||
FsPermissionStore, FsPluginPackageStore, FsPluginRegistryStore, FsPluginStorageStore,
|
||||
FsProfileStore, FsProjectPluginSettingsStore, FsProjectStore, FsProviderSessionStore,
|
||||
FsSecretStore, FsSkillStore, FsSprintStore, FsSystemPermissionStore, FsTemplateStore,
|
||||
FsWindowStateStore, Git2Repository, HeuristicHandoffSummarizer, HfModelArtifactDownloader,
|
||||
HttpOpenAiCompatibleProbe, HttpProviderModelCatalogue, IdeaiContextStore,
|
||||
InMemoryConversationRegistry, InMemoryMailbox, InMemoryPairAttemptLimiter, LlamaCppRuntime,
|
||||
LocalEnvironmentReader, LocalFileSystem, LocalManagedProcess, LocalProcessSpawner,
|
||||
ManagedTempToolProvider, McpServer, MediatedInbox, NaiveMemoryRecall, OrchestratorWatchHandle,
|
||||
PortablePtyAdapter, ProcessCliVersionReader, ReadOnlyRuntimePermissionProbe, RwFileGuard,
|
||||
StructuredSessionFactory, SystemClock, SystemMillisClock, TemplateToolProvider,
|
||||
TicketAssistantEnvironmentPreparer, TicketToolProvider, TokioBroadcastEventBus, TokioScheduler,
|
||||
ToolPolicyRegistry, UuidGenerator, VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL,
|
||||
ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
|
||||
FsMcpToolPermissionStore, FsMemoryStore, FsModelServerRegistry, FsOpenProjectRestoreStore,
|
||||
FsOrchestratorWatcher, FsPermissionStore, FsPluginPackageStore, FsPluginRegistryStore,
|
||||
FsPluginStorageStore, FsProfileStore, FsProjectPluginSettingsStore, FsProjectStore,
|
||||
FsProviderSessionStore, FsSecretStore, FsSkillStore, FsSprintStore, FsSystemPermissionStore,
|
||||
FsTemplateStore, FsWindowStateStore, Git2Repository, HeuristicHandoffSummarizer,
|
||||
HfModelArtifactDownloader, HttpOpenAiCompatibleProbe, HttpProviderModelCatalogue,
|
||||
IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox, InMemoryPairAttemptLimiter,
|
||||
LlamaCppRuntime, LocalEnvironmentReader, LocalFileSystem, LocalManagedProcess,
|
||||
LocalProcessSpawner, ManagedTempToolProvider, McpServer, MediatedInbox, NaiveMemoryRecall,
|
||||
OrchestratorWatchHandle, PortablePtyAdapter, ProcessCliVersionReader,
|
||||
ReadOnlyRuntimePermissionProbe, RwFileGuard, StructuredSessionFactory, SystemClock,
|
||||
SystemMillisClock, TemplateToolProvider, TicketAssistantEnvironmentPreparer,
|
||||
TicketToolProvider, TokioBroadcastEventBus, TokioScheduler, ToolPolicyRegistry, UuidGenerator,
|
||||
VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS,
|
||||
VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
|
||||
};
|
||||
|
||||
pub mod dto;
|
||||
@ -1789,6 +1793,20 @@ pub struct BackendCore {
|
||||
pub open_project: Arc<OpenProject>,
|
||||
/// Close a project (persist state).
|
||||
pub close_project: Arc<CloseProject>,
|
||||
/// Read the ordered open-project restore intent.
|
||||
pub read_open_project_restore_state: Arc<ReadOpenProjectRestoreState>,
|
||||
/// Record a visibly opened project.
|
||||
pub record_project_opened_for_restore: Arc<RecordProjectOpenedForRestore>,
|
||||
/// Record an explicit project close.
|
||||
pub record_project_closed_for_restore: Arc<RecordProjectClosedForRestore>,
|
||||
/// Persist a new visible project order.
|
||||
pub save_open_project_restore_order: Arc<SaveOpenProjectRestoreOrder>,
|
||||
/// Restore every retained project.
|
||||
pub restore_open_projects: Arc<RestoreOpenProjects>,
|
||||
/// Retry one or all retained restore failures.
|
||||
pub retry_open_project_restore: Arc<RetryOpenProjectRestore>,
|
||||
/// Remove one retained restore entry.
|
||||
pub remove_project_from_restore: Arc<application::RemoveProjectFromRestore>,
|
||||
/// Close a tab.
|
||||
pub close_tab: Arc<CloseTab>,
|
||||
/// List known projects.
|
||||
@ -2342,6 +2360,10 @@ impl BackendCore {
|
||||
Arc::clone(&fs) as Arc<dyn FileSystem>,
|
||||
app_data_dir.to_string_lossy().into_owned(),
|
||||
));
|
||||
let open_project_restore_store = Arc::new(FsOpenProjectRestoreStore::new(
|
||||
Arc::clone(&fs) as Arc<dyn FileSystem>,
|
||||
app_data_dir.to_string_lossy().into_owned(),
|
||||
));
|
||||
let window_state_store = Arc::new(FsWindowStateStore::new(
|
||||
Arc::clone(&fs) as Arc<dyn FileSystem>,
|
||||
app_data_dir.to_string_lossy().into_owned(),
|
||||
@ -2357,6 +2379,8 @@ impl BackendCore {
|
||||
// Port-typed handles for injection.
|
||||
let fs_port = Arc::clone(&fs) as Arc<dyn FileSystem>;
|
||||
let store_port = Arc::clone(&store) as Arc<dyn ProjectStore>;
|
||||
let open_project_restore_store_port =
|
||||
Arc::clone(&open_project_restore_store) as Arc<dyn OpenProjectRestoreStore>;
|
||||
let window_state_port = Arc::clone(&window_state_store) as Arc<dyn WindowStateStore>;
|
||||
let device_session_port = Arc::clone(&device_session_store) as Arc<dyn DeviceSessionStore>;
|
||||
let pair_attempt_limiter_port =
|
||||
@ -2429,6 +2453,33 @@ impl BackendCore {
|
||||
Arc::clone(&fs_port),
|
||||
));
|
||||
let close_project = Arc::new(CloseProject::new(Arc::clone(&store_port)));
|
||||
let open_project_restore_service = Arc::new(application::OpenProjectRestoreService::new(
|
||||
Arc::clone(&open_project_restore_store_port),
|
||||
Arc::clone(&store_port),
|
||||
Arc::clone(&fs_port),
|
||||
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
));
|
||||
let read_open_project_restore_state = Arc::new(ReadOpenProjectRestoreState::new(
|
||||
Arc::clone(&open_project_restore_service),
|
||||
));
|
||||
let record_project_opened_for_restore = Arc::new(RecordProjectOpenedForRestore::new(
|
||||
Arc::clone(&open_project_restore_service),
|
||||
));
|
||||
let record_project_closed_for_restore = Arc::new(RecordProjectClosedForRestore::new(
|
||||
Arc::clone(&open_project_restore_service),
|
||||
));
|
||||
let save_open_project_restore_order = Arc::new(SaveOpenProjectRestoreOrder::new(
|
||||
Arc::clone(&open_project_restore_service),
|
||||
));
|
||||
let restore_open_projects = Arc::new(RestoreOpenProjects::new(Arc::clone(
|
||||
&open_project_restore_service,
|
||||
)));
|
||||
let retry_open_project_restore = Arc::new(RetryOpenProjectRestore::new(Arc::clone(
|
||||
&open_project_restore_service,
|
||||
)));
|
||||
let remove_project_from_restore = Arc::new(application::RemoveProjectFromRestore::new(
|
||||
Arc::clone(&open_project_restore_service),
|
||||
));
|
||||
let close_tab = Arc::new(CloseTab::new(Arc::clone(&store_port)));
|
||||
let list_projects = Arc::new(ListProjects::new(Arc::clone(&store_port)));
|
||||
let list_project_files = Arc::new(ListProjectFiles::new(Arc::clone(&fs_port)));
|
||||
@ -4017,6 +4068,13 @@ impl BackendCore {
|
||||
create_project,
|
||||
open_project,
|
||||
close_project,
|
||||
read_open_project_restore_state,
|
||||
record_project_opened_for_restore,
|
||||
record_project_closed_for_restore,
|
||||
save_open_project_restore_order,
|
||||
restore_open_projects,
|
||||
retry_open_project_restore,
|
||||
remove_project_from_restore,
|
||||
close_tab,
|
||||
list_projects,
|
||||
list_project_files,
|
||||
@ -4216,6 +4274,63 @@ impl BackendCore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens one project, persists its visible-open intent, then applies the
|
||||
/// complete activation side-effect sequence shared by every driving adapter.
|
||||
pub async fn open_project_and_activate(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
) -> Result<application::OpenProjectOutput, AppError> {
|
||||
let output = self
|
||||
.open_project
|
||||
.execute(application::OpenProjectInput { project_id })
|
||||
.await?;
|
||||
self.record_project_opened_for_restore
|
||||
.execute(output.project.clone())
|
||||
.await?;
|
||||
self.activate_project(&output.project).await;
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Applies the same best-effort runtime activation to a normally opened,
|
||||
/// newly created, restored, or retried project.
|
||||
pub async fn activate_project(&self, project: &Project) {
|
||||
let project_id = project.id;
|
||||
let _ = self
|
||||
.reconcile_layouts
|
||||
.execute(application::ReconcileLayoutsInput { project_id })
|
||||
.await;
|
||||
let _ = self
|
||||
.reconcile_live_state
|
||||
.execute(application::ReconcileLiveStateInput { project_id })
|
||||
.await;
|
||||
let _ = self.reconcile_background_tasks.execute(project_id).await;
|
||||
self.ensure_orchestrator_watch(project);
|
||||
self.reconcile_claude_run_dirs(project).await;
|
||||
}
|
||||
|
||||
/// Restores every retained project and activates each successful result.
|
||||
pub async fn restore_open_projects_and_activate(
|
||||
&self,
|
||||
) -> Result<RestoreOpenProjectsOutput, AppError> {
|
||||
let output = self.restore_open_projects.execute().await?;
|
||||
for project in &output.opened {
|
||||
self.activate_project(project).await;
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Retries retained failures (or one requested entry) and activates successes.
|
||||
pub async fn retry_open_project_restore_and_activate(
|
||||
&self,
|
||||
project_id: Option<ProjectId>,
|
||||
) -> Result<RestoreOpenProjectsOutput, AppError> {
|
||||
let output = self.retry_open_project_restore.execute(project_id).await?;
|
||||
for project in &output.opened {
|
||||
self.activate_project(project).await;
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Starts a [`FsOrchestratorWatcher`] for `project` if one is not already
|
||||
/// running for it (idempotent). The watcher tails the project's
|
||||
/// `.ideai/requests/` tree and dispatches each request through the shared
|
||||
@ -8168,3 +8283,166 @@ mod bind_endpoint_d1_tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod open_project_restore_tests {
|
||||
use super::BackendCore;
|
||||
use application::CreateProjectInput;
|
||||
use domain::{OpenProjectRestoreFailureCode, Project, ProjectId, ProjectPath, RemoteRef};
|
||||
use std::path::{Path, PathBuf};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn temp_path(tag: &str) -> PathBuf {
|
||||
std::env::temp_dir().join(format!("idea-restore-{tag}-{}", Uuid::new_v4()))
|
||||
}
|
||||
|
||||
async fn create_project(core: &BackendCore, root: &Path, name: &str) -> Project {
|
||||
core.create_project
|
||||
.execute(CreateProjectInput {
|
||||
name: name.to_owned(),
|
||||
root: root.to_string_lossy().into_owned(),
|
||||
remote: None,
|
||||
default_profile_id: None,
|
||||
})
|
||||
.await
|
||||
.expect("project creation succeeds")
|
||||
.project
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn restore_state_preserves_order_failures_retry_close_and_remove() {
|
||||
let app_data = temp_path("appdata");
|
||||
let roots = temp_path("roots");
|
||||
let root_a = roots.join("a");
|
||||
let root_b = roots.join("b");
|
||||
let core = BackendCore::build(app_data.clone());
|
||||
|
||||
let empty = core
|
||||
.restore_open_projects_and_activate()
|
||||
.await
|
||||
.expect("missing restore file is an empty successful restore");
|
||||
assert!(empty.opened.is_empty());
|
||||
assert!(empty.failures.is_empty());
|
||||
assert!(!app_data.join("open-projects.json").exists());
|
||||
|
||||
let project_a = create_project(&core, &root_a, "A").await;
|
||||
let project_b = create_project(&core, &root_b, "B").await;
|
||||
core.record_project_opened_for_restore
|
||||
.execute(project_a.clone())
|
||||
.await
|
||||
.expect("A is recorded");
|
||||
core.record_project_opened_for_restore
|
||||
.execute(project_b.clone())
|
||||
.await
|
||||
.expect("B is recorded");
|
||||
|
||||
let reordered = core
|
||||
.save_open_project_restore_order
|
||||
.execute(vec![project_b.id, project_a.id])
|
||||
.await
|
||||
.expect("order is saved");
|
||||
assert_eq!(
|
||||
reordered
|
||||
.entries
|
||||
.iter()
|
||||
.map(|entry| entry.project_id)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![project_b.id, project_a.id]
|
||||
);
|
||||
|
||||
core.record_project_closed_for_restore
|
||||
.execute(project_b.id)
|
||||
.await
|
||||
.expect("explicit close removes B");
|
||||
let after_close = core
|
||||
.read_open_project_restore_state
|
||||
.execute()
|
||||
.await
|
||||
.expect("state is readable");
|
||||
assert_eq!(after_close.entries.len(), 1);
|
||||
assert_eq!(after_close.entries[0].project_id, project_a.id);
|
||||
|
||||
core.record_project_opened_for_restore
|
||||
.execute(project_b.clone())
|
||||
.await
|
||||
.expect("reopening B appends it");
|
||||
std::fs::remove_dir_all(&root_b).expect("temporary B root is removed");
|
||||
|
||||
let restored = core
|
||||
.restore_open_projects_and_activate()
|
||||
.await
|
||||
.expect("one inaccessible project does not abort restore");
|
||||
assert_eq!(
|
||||
restored
|
||||
.opened
|
||||
.iter()
|
||||
.map(|project| project.id)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![project_a.id]
|
||||
);
|
||||
assert_eq!(restored.failures.len(), 1);
|
||||
assert_eq!(restored.failures[0].project_id, project_b.id);
|
||||
assert_eq!(
|
||||
restored.failures[0].code,
|
||||
OpenProjectRestoreFailureCode::Inaccessible
|
||||
);
|
||||
assert!(core.open_project_ids().contains(&project_a.id));
|
||||
assert!(core
|
||||
.mcp_servers
|
||||
.lock()
|
||||
.expect("mcp server registry")
|
||||
.contains_key(&project_a.id));
|
||||
|
||||
std::fs::create_dir_all(&root_b).expect("temporary B root is restored");
|
||||
let retried = core
|
||||
.retry_open_project_restore_and_activate(Some(project_b.id))
|
||||
.await
|
||||
.expect("targeted retry succeeds");
|
||||
assert_eq!(retried.opened.len(), 1);
|
||||
assert_eq!(retried.opened[0].id, project_b.id);
|
||||
assert!(core
|
||||
.mcp_servers
|
||||
.lock()
|
||||
.expect("mcp server registry")
|
||||
.contains_key(&project_b.id));
|
||||
|
||||
let unknown_id = ProjectId::from_uuid(Uuid::new_v4());
|
||||
let unknown = Project::new(
|
||||
unknown_id,
|
||||
"Missing",
|
||||
ProjectPath::new(roots.join("missing").to_string_lossy().into_owned())
|
||||
.expect("absolute test path"),
|
||||
RemoteRef::local(),
|
||||
0,
|
||||
)
|
||||
.expect("valid retained project snapshot");
|
||||
core.record_project_opened_for_restore
|
||||
.execute(unknown)
|
||||
.await
|
||||
.expect("unknown snapshot can be retained");
|
||||
let with_missing = core
|
||||
.restore_open_projects
|
||||
.execute()
|
||||
.await
|
||||
.expect("missing registry entry does not abort restore");
|
||||
assert_eq!(
|
||||
with_missing.failures.last().map(|failure| failure.code),
|
||||
Some(OpenProjectRestoreFailureCode::NotFound)
|
||||
);
|
||||
|
||||
let removed = core
|
||||
.remove_project_from_restore
|
||||
.execute(unknown_id)
|
||||
.await
|
||||
.expect("retained failure can be removed");
|
||||
assert!(removed
|
||||
.entries
|
||||
.iter()
|
||||
.all(|entry| entry.project_id != unknown_id));
|
||||
|
||||
core.stop_orchestrator_watch(&project_a.id);
|
||||
core.stop_orchestrator_watch(&project_b.id);
|
||||
let _ = std::fs::remove_dir_all(&app_data);
|
||||
let _ = std::fs::remove_dir_all(&roots);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user