From 8563f4a37190741e5b91fa1c538a6d15d4c368e3 Mon Sep 17 00:00:00 2001 From: shweta2101 Date: Wed, 14 Oct 2026 11:51:26 +0530 Subject: [PATCH] Report OSM cleanup failure on workspace delete Make workspace deletion resilient to OSM cleanup failures: introduce DeleteWorkspaceResult and change WorkspacesClient.deleteWorkspace to use Promise.allSettled so the API record deletion and the OSM cleanup are attempted in parallel. The method now throws if the primary workspace delete fails and returns { osmRequestSucceeded: boolean } to indicate whether the OSM request succeeded. Update the Delete settings panel to show an informational toast when the workspace is deleted but OSM cleanup failed. Add unit tests for the new delete behavior and an e2e test that asserts the info toast is shown when OSM cleanup returns an error. --- components/settings/panel/Delete.vue | 10 ++++++- services/workspaces.ts | 12 ++++++-- test/e2e/settings.spec.ts | 31 +++++++++++++++++++ test/unit/services/workspaces.test.ts | 43 +++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 3 deletions(-) diff --git a/components/settings/panel/Delete.vue b/components/settings/panel/Delete.vue index 679a246d..e8edb929 100644 --- a/components/settings/panel/Delete.vue +++ b/components/settings/panel/Delete.vue @@ -96,7 +96,15 @@ async function submitDelete() { isDeleting.value = true; try { - await workspacesClient.deleteWorkspace(workspace.id); + const { osmRequestSucceeded: osmCleanupSucceeded } = await workspacesClient.deleteWorkspace(workspace.id); + + if (!osmCleanupSucceeded) { + toast.info( + 'The workspace has been deleted, but the data may persist for some more time.', + { clearOnUrlChange: false } + ); + } + await navigateTo('/dashboard'); } catch (error) { diff --git a/services/workspaces.ts b/services/workspaces.ts index c2032c2f..e6d03d0f 100644 --- a/services/workspaces.ts +++ b/services/workspaces.ts @@ -79,6 +79,10 @@ export class WorkspacesClientError extends Error { } } +export interface DeleteWorkspaceResult { + osmRequestSucceeded: boolean; +} + export class WorkspacesClient extends BaseHttpClient implements ICancelableClient { #tdeiClient: TdeiClient; #osmClient: OsmApiClient; @@ -241,11 +245,15 @@ export class WorkspacesClient extends BaseHttpClient implements ICancelableClien ); } - async deleteWorkspace(id: WorkspaceId): Promise { - await Promise.all([ + async deleteWorkspace(id: WorkspaceId): Promise { + const [workspaceDelete, osmDelete] = await Promise.allSettled([ this._delete(`workspaces/${id}`), this.#osmClient.deleteWorkspace(id), ]); + if (workspaceDelete.status === 'rejected') { + throw workspaceDelete.reason; + } + return { osmRequestSucceeded: osmDelete.status === 'fulfilled' }; } async getLongFormQuestSettings(id: WorkspaceId): Promise { diff --git a/test/e2e/settings.spec.ts b/test/e2e/settings.spec.ts index 46abcd4f..d283fdf1 100644 --- a/test/e2e/settings.spec.ts +++ b/test/e2e/settings.spec.ts @@ -104,6 +104,9 @@ function successToast(page: Page) { function errorToast(page: Page) { return page.locator('.Toastify__toast--error'); } +function infoToast(page: Page) { + return page.locator('.Toastify__toast--info'); +} test.describe('workspace settings', () => { // @test e2e: the workspace can be renamed with the "Workspace Title" field, then clicking "Rename"; @@ -433,6 +436,34 @@ test.describe('workspace settings', () => { await expect(page).toHaveURL(/\/dashboard$/); }); + test('shows an info toast when deletion succeeds but OSM cleanup fails', async ({ page }) => { + await seedAuthenticatedSession(page); + await stubSettings(page); + + await page.route('**/workspaces/1', async (route) => { + if (route.request().method() === 'DELETE') { + return route.fulfill({ status: 204, body: '' }); + } + return route.fallback(); + }); + await page.route('**/osm/**', (route) => { + return route.fulfill({ status: 403, body: 'Forbidden' }); + }); + + await page.goto('/workspace/1/settings'); + + const del = page.locator('div.card', { hasText: 'Delete Workspace' }); + await del.getByRole('button', { name: /^I understand/ }).click(); + await del.getByRole('textbox').fill('delete'); + await del.getByRole('button', { name: 'Delete this workspace', exact: true }).click(); + + await expect(page).toHaveURL(/\/dashboard$/); + await expect(infoToast(page)).toContainText( + 'The workspace has been deleted, but the data may persist for some more time.' + ); + await expect(errorToast(page)).toHaveCount(0); + }); + // @test e2e: validate that all the API calls used on this page match the Swagger spec and that // success and error states are handled properly with toasts (snapshot these). test('all API calls conform to the OpenAPI spec', async ({ page }) => { diff --git a/test/unit/services/workspaces.test.ts b/test/unit/services/workspaces.test.ts index bfeade8a..cff1cccf 100644 --- a/test/unit/services/workspaces.test.ts +++ b/test/unit/services/workspaces.test.ts @@ -174,6 +174,49 @@ describe('WorkspacesClient.createBlankWorkspace', () => { }); }); +describe('WorkspacesClient.deleteWorkspace', () => { + it('reports failed OSM cleanup after the workspace record is deleted', async () => { + const deleteOsmWorkspace = vi.fn().mockRejectedValue(new Error('Forbidden')); + const client = new WorkspacesClient( + TEST_API_BASE, + TEST_API_BASE, + tdeiClient, + { deleteWorkspace: deleteOsmWorkspace } as unknown as OsmApiClient + ); + + server.use( + http.delete(`${TEST_API_BASE}workspaces/1`, () => { + return new HttpResponse(null, { status: 204 }); + }) + ); + + await expect(client.deleteWorkspace(1)).resolves.toEqual({ + osmRequestSucceeded: false + }); + expect(deleteOsmWorkspace).toHaveBeenCalledWith(1); + }); + + it('still rejects when the workspace record cannot be deleted', async () => { + const deleteOsmWorkspace = vi.fn().mockResolvedValue(undefined); + const client = new WorkspacesClient( + TEST_API_BASE, + TEST_API_BASE, + tdeiClient, + { deleteWorkspace: deleteOsmWorkspace } as unknown as OsmApiClient + ); + + server.use( + http.delete(`${TEST_API_BASE}workspaces/1`, () => { + return new HttpResponse(null, { status: 500, statusText: 'Server Error' }); + }) + ); + + await expect(client.deleteWorkspace(1)).rejects.toBeInstanceOf( + WorkspacesClientError + ); + }); +}); + describe('WorkspacesClient.checkWorkspaceTitleAvailability', () => { const LEGACY_API_BASE = 'http://legacy-api.test/'; const NEW_API_BASE = 'http://new-api.test/';