fix(desktop): retire old-name renderer sockets and widen the deletion gate for renames
Follow-ups on top of the salvaged rename lifecycle: - Retire both local renderer socket scopes for the OLD profile name before the rename PATCH (rename-profile-dialog), mirroring the delete-path fix from #88638 — a retained socket otherwise treats the rename's backend teardown as a transient drop and its reconnect respawns the old-name backend, whose ensure_hermes_home() recreates the directory the rename just moved (#45474). - Hold the profile deletion gate for rename requests too, so a concurrent renderer reconnect entering ensureBackend() mid-rename cannot respawn the old-name backend. - Regression tests for retire-before-rename ordering and the validation-rejected path.
This commit is contained in:
@@ -248,6 +248,7 @@ import {
|
||||
profileNameFromDeleteRequest,
|
||||
resolveRouteProfile
|
||||
} from './profile-delete-routing'
|
||||
import { prepareProfileRenameLifecycle, profileRenameFromRequest } from './profile-rename-routing'
|
||||
import {
|
||||
buildSidebarSessionSliceParams,
|
||||
fetchPrimaryProfileSessions,
|
||||
@@ -265,7 +266,6 @@ import {
|
||||
} from './remote-liveness'
|
||||
import { missingRendererAssets } from './renderer-bundle'
|
||||
import { attachRendererConsoleCapture, formatRendererBoundaryReport } from './renderer-log'
|
||||
import { prepareProfileRenameLifecycle, profileRenameFromRequest } from './profile-rename-routing'
|
||||
import {
|
||||
buildSessionWindowUrl,
|
||||
chatWindowWebPreferences,
|
||||
@@ -13260,6 +13260,7 @@ async function handleHermesApiRequest(request) {
|
||||
// primary rename the lifecycle has already made `default` the temporary
|
||||
// primary until the PATCH settles, so the request routes there.
|
||||
const apiRoute = resolveProfileApiRequest(profile, request.path, profileRouteOptions(profile, request))
|
||||
|
||||
const routeProfile = profileRename
|
||||
? profileRename.routeProfile
|
||||
: resolveRouteProfile(tornDownProfile, apiRoute.backendProfile)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
|
||||
import { renameProfile } from '@/hermes'
|
||||
import { retireLocalProfileGateways } from '@/store/gateway'
|
||||
|
||||
import { RenameProfileDialog } from './rename-profile-dialog'
|
||||
|
||||
// Pins the rename half of the deleted-profile-resurrection class (#88638 fixed
|
||||
// the delete half): a retained renderer socket for the OLD profile name must be
|
||||
// retired BEFORE the rename PATCH tears down its backend, or the socket's
|
||||
// reconnect loop respawns the old-name backend and recreates the directory the
|
||||
// rename just moved.
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
vi.mock('@/hermes', () => ({
|
||||
renameProfile: vi.fn(async () => ({ name: 'renamed', ok: true, path: '/x' }))
|
||||
}))
|
||||
|
||||
vi.mock('@/store/gateway', () => ({
|
||||
retireLocalProfileGateways: vi.fn()
|
||||
}))
|
||||
|
||||
it('retires the old-name local gateways before issuing the rename', async () => {
|
||||
const order: string[] = []
|
||||
|
||||
vi.mocked(retireLocalProfileGateways).mockImplementationOnce(() => {
|
||||
order.push('retire')
|
||||
})
|
||||
vi.mocked(renameProfile).mockImplementationOnce(async () => {
|
||||
order.push('rename')
|
||||
|
||||
return { name: 'renamed', ok: true, path: '/x' }
|
||||
})
|
||||
|
||||
render(<RenameProfileDialog currentName="selena" onClose={vi.fn()} open />)
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/new name/i), { target: { value: 'renamed' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: /^rename$/i }))
|
||||
|
||||
await waitFor(() => expect(renameProfile).toHaveBeenCalledWith('selena', 'renamed'))
|
||||
expect(retireLocalProfileGateways).toHaveBeenCalledWith('selena')
|
||||
expect(order).toEqual(['retire', 'rename'])
|
||||
})
|
||||
|
||||
it('does not retire gateways when validation rejects the submit', async () => {
|
||||
render(<RenameProfileDialog currentName="selena" onClose={vi.fn()} open />)
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/new name/i), { target: { value: '' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: /^rename$/i }))
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Name is required.')).toBeTruthy())
|
||||
expect(retireLocalProfileGateways).not.toHaveBeenCalled()
|
||||
expect(renameProfile).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -16,6 +16,7 @@ import { renameProfile } from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { AlertTriangle } from '@/lib/icons'
|
||||
import { slug } from '@/lib/sanitize'
|
||||
import { retireLocalProfileGateways } from '@/store/gateway'
|
||||
|
||||
import { isValidProfileName } from './create-profile-dialog'
|
||||
|
||||
@@ -72,6 +73,11 @@ export function RenameProfileDialog({
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// A retained renderer socket for the old name would treat the rename's
|
||||
// backend teardown as a transient drop and redial, resurrecting the
|
||||
// old-name backend whose ensure_hermes_home() recreates the directory
|
||||
// the rename just moved (same class as the delete path, #88638).
|
||||
retireLocalProfileGateways(currentName)
|
||||
await renameProfile(currentName, trimmed)
|
||||
await onRenamed?.(trimmed)
|
||||
setStatus('done')
|
||||
|
||||
Reference in New Issue
Block a user