Release simulator launch pending guard on tab creation failure (#7682)
This commit is contained in:
parent
5da02b0105
commit
fcdcab7cd4
|
|
@ -194,4 +194,74 @@ describe('openMobileEmulatorTab', () => {
|
|||
expect(callRuntimeRpc).not.toHaveBeenCalled()
|
||||
expect(ensureSimulatorTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('clears manual launch pending state when the simulator tab cannot be created', async () => {
|
||||
vi.mocked(ensureSimulatorTab).mockReturnValue(null)
|
||||
|
||||
await expect(openMobileEmulatorTab('wt-1', { targetGroupId: 'group-1' })).resolves.toBeNull()
|
||||
|
||||
expect(callRuntimeRpc).not.toHaveBeenCalled()
|
||||
expect(isManualSimulatorLaunchPending('wt-1')).toBe(false)
|
||||
})
|
||||
|
||||
it('clears manual launch pending state when simulator tab creation throws', async () => {
|
||||
vi.mocked(ensureSimulatorTab).mockImplementation(() => {
|
||||
throw new Error('split group missing')
|
||||
})
|
||||
|
||||
await expect(openMobileEmulatorTab('wt-1', { targetGroupId: 'group-1' })).rejects.toThrow(
|
||||
'split group missing'
|
||||
)
|
||||
|
||||
expect(callRuntimeRpc).not.toHaveBeenCalled()
|
||||
expect(isManualSimulatorLaunchPending('wt-1')).toBe(false)
|
||||
})
|
||||
|
||||
it('allows a successful retry after simulator tab creation returns null', async () => {
|
||||
vi.mocked(ensureSimulatorTab).mockReturnValueOnce(null).mockReturnValueOnce('sim-retry')
|
||||
vi.mocked(callRuntimeRpc).mockResolvedValue(mockAttachResult)
|
||||
|
||||
await expect(openMobileEmulatorTab('wt-1', { targetGroupId: 'group-1' })).resolves.toBeNull()
|
||||
await expect(openMobileEmulatorTab('wt-1', { targetGroupId: 'group-1' })).resolves.toBe(
|
||||
'sim-retry'
|
||||
)
|
||||
|
||||
expect(ensureSimulatorTab).toHaveBeenCalledTimes(2)
|
||||
const attachCalls = vi.mocked(callRuntimeRpc).mock.calls.filter(([, method]) =>
|
||||
method === 'emulator.attach'
|
||||
)
|
||||
expect(attachCalls).toHaveLength(1)
|
||||
expect(isManualSimulatorLaunchPending('wt-1')).toBe(false)
|
||||
})
|
||||
|
||||
it('does not release another in-flight launch when duplicate tab creation throws', async () => {
|
||||
let resolveAttach: (value: typeof mockAttachResult) => void = () => {}
|
||||
vi.mocked(callRuntimeRpc).mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveAttach = resolve
|
||||
})
|
||||
)
|
||||
vi.mocked(ensureSimulatorTab)
|
||||
.mockImplementationOnce(() => {
|
||||
mockStoreState.unifiedTabsByWorktree['wt-1'] = [{
|
||||
id: 'sim-1',
|
||||
contentType: 'simulator'
|
||||
}]
|
||||
return 'sim-1'
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error('split group missing')
|
||||
})
|
||||
|
||||
const firstLaunch = openMobileEmulatorTab('wt-1', { targetGroupId: 'group-1' })
|
||||
await expect(openMobileEmulatorTab('wt-1', { targetGroupId: 'group-1' })).rejects.toThrow(
|
||||
'split group missing'
|
||||
)
|
||||
|
||||
expect(isManualSimulatorLaunchPending('wt-1')).toBe(true)
|
||||
resolveAttach(mockAttachResult)
|
||||
await firstLaunch
|
||||
expect(isManualSimulatorLaunchPending('wt-1')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -72,46 +72,55 @@ export async function openMobileEmulatorTab(
|
|||
|
||||
cancelPendingSimulatorPaneShutdown(worktreeId)
|
||||
const alreadyLaunching = isManualSimulatorLaunchPending(worktreeId)
|
||||
if (!alreadyLaunching) {
|
||||
beginManualSimulatorLaunch(worktreeId)
|
||||
if (alreadyLaunching) {
|
||||
return ensureSimulatorTab(worktreeId, {
|
||||
placement: options.placement ?? 'rightSplit',
|
||||
targetGroupId,
|
||||
surfacePane: true
|
||||
})
|
||||
}
|
||||
const tabId = ensureSimulatorTab(worktreeId, {
|
||||
placement: options.placement ?? 'rightSplit',
|
||||
targetGroupId,
|
||||
surfacePane: true
|
||||
})
|
||||
if (!tabId || alreadyLaunching) {
|
||||
return tabId
|
||||
}
|
||||
dispatchManualSimulatorLaunchStarted(worktreeId)
|
||||
beginManualSimulatorLaunch(worktreeId)
|
||||
try {
|
||||
// Why: the pane is visible but inert while serve-sim settles; the actual
|
||||
// stream is handed to it only after attach returns ready info.
|
||||
const result = await callRuntimeRpc<EmulatorAttachResult>(
|
||||
{ kind: 'local' },
|
||||
'emulator.attach',
|
||||
{
|
||||
worktree: worktreeId,
|
||||
focus: false
|
||||
}
|
||||
)
|
||||
if (!result.attached || !result.info) {
|
||||
throw new Error('Could not start the emulator.')
|
||||
// Why: this scope owns the guard from before tab creation through attach;
|
||||
// every early return or error must release it so a retry can proceed.
|
||||
const tabId = ensureSimulatorTab(worktreeId, {
|
||||
placement: options.placement ?? 'rightSplit',
|
||||
targetGroupId,
|
||||
surfacePane: true
|
||||
})
|
||||
if (!tabId) {
|
||||
return null
|
||||
}
|
||||
// Why: users can close the tab while serve-sim is still starting; after
|
||||
// attach registers the managed session, clean it up if no pane remains.
|
||||
if (await shutdownManagedSimulatorIfNoPane(worktreeId, tabId)) {
|
||||
dispatchManualSimulatorLaunchStarted(worktreeId)
|
||||
try {
|
||||
// Why: the pane is visible but inert while serve-sim settles; the actual
|
||||
// stream is handed to it only after attach returns ready info.
|
||||
const result = await callRuntimeRpc<EmulatorAttachResult>(
|
||||
{ kind: 'local' },
|
||||
'emulator.attach',
|
||||
{
|
||||
worktree: worktreeId,
|
||||
focus: false
|
||||
}
|
||||
)
|
||||
if (!result.attached || !result.info) {
|
||||
throw new Error('Could not start the emulator.')
|
||||
}
|
||||
// Why: users can close the tab while serve-sim is still starting; after
|
||||
// attach registers the managed session, clean it up if no pane remains.
|
||||
if (await shutdownManagedSimulatorIfNoPane(worktreeId, tabId)) {
|
||||
return tabId
|
||||
}
|
||||
|
||||
rememberPrelaunchedSimulatorSession(worktreeId, result.info)
|
||||
dispatchPrelaunchedSession(worktreeId, result.info)
|
||||
return tabId
|
||||
} catch (error) {
|
||||
const message = getLaunchErrorMessage(error)
|
||||
toast.error(message)
|
||||
dispatchManualSimulatorLaunchFailed(worktreeId, message)
|
||||
return tabId
|
||||
}
|
||||
|
||||
rememberPrelaunchedSimulatorSession(worktreeId, result.info)
|
||||
dispatchPrelaunchedSession(worktreeId, result.info)
|
||||
return tabId
|
||||
} catch (error) {
|
||||
const message = getLaunchErrorMessage(error)
|
||||
toast.error(message)
|
||||
dispatchManualSimulatorLaunchFailed(worktreeId, message)
|
||||
return tabId
|
||||
} finally {
|
||||
finishManualSimulatorLaunch(worktreeId)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue