mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(runtime): keep desktop take-back from stale phone input
Invalidate unfinished phone input claims after desktop takeback or disconnect. Recheck presence after delayed layout and preserve fresh phone input. Closes #23199. Adapted from #23200. Co-authored-by: Tauri-EPO <enrico.pin@gmail.com>
This commit is contained in:
@@ -280,6 +280,60 @@ describe('mobile presence lock — driver state machine', () => {
|
||||
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'mobile', clientId: 'phone-B' })
|
||||
})
|
||||
|
||||
it.each(['before commit', 'during commit', 'direct phone-fit'] as const)(
|
||||
'desktop take-back supersedes mobile input %s',
|
||||
async (timing) => {
|
||||
const { runtime, driverEvents, ptySizes } = createRuntime()
|
||||
await runtime.handleMobileSubscribe('pty-1', 'phone-A', { cols: 45, rows: 20 })
|
||||
await runtime.reclaimTerminalForDesktop('pty-1')
|
||||
|
||||
const claim =
|
||||
timing === 'direct phone-fit' ? null : runtime.beginMobileInputFloor('pty-1', 'phone-A')!
|
||||
let finishLayout = () => {}
|
||||
let committing: Promise<void> | undefined
|
||||
if (timing !== 'before commit') {
|
||||
vi.spyOn(runtime, 'applyMobileDisplayMode').mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
finishLayout = () => resolve(true)
|
||||
})
|
||||
)
|
||||
committing = claim ? claim.commit() : runtime.mobileTookFloor('pty-1', 'phone-A')
|
||||
}
|
||||
|
||||
await runtime.reclaimTerminalForDesktop('pty-1')
|
||||
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'desktop' })
|
||||
finishLayout()
|
||||
await (committing ?? claim!.commit())
|
||||
|
||||
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'desktop' })
|
||||
expect(driverEvents.at(-1)?.driver).toEqual({ kind: 'desktop' })
|
||||
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
|
||||
|
||||
await runtime.beginMobileInputFloor('pty-1', 'phone-A')!.commit()
|
||||
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'mobile', clientId: 'phone-A' })
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['commit', 'rollback'] as const)(
|
||||
'a superseded mobile input %s preserves a newer phone claim',
|
||||
async (settle) => {
|
||||
const { runtime, driverEvents } = createRuntime()
|
||||
await runtime.handleMobileSubscribe('pty-1', 'phone-A', { cols: 45, rows: 20 })
|
||||
await runtime.handleMobileSubscribe('pty-1', 'phone-B', { cols: 60, rows: 25 })
|
||||
await runtime.reclaimTerminalForDesktop('pty-1')
|
||||
const stale = runtime.beginMobileInputFloor('pty-1', 'phone-A')!
|
||||
await runtime.reclaimTerminalForDesktop('pty-1')
|
||||
const fresh = runtime.beginMobileInputFloor('pty-1', 'phone-B')!
|
||||
|
||||
await stale[settle]()
|
||||
await fresh.commit()
|
||||
|
||||
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'mobile', clientId: 'phone-B' })
|
||||
expect(driverEvents.at(-1)?.driver).toEqual({ kind: 'mobile', clientId: 'phone-B' })
|
||||
}
|
||||
)
|
||||
|
||||
it('mobile input without an active subscriber cannot create an orphaned floor lock', async () => {
|
||||
const { runtime } = createRuntime()
|
||||
await runtime.handleMobileSubscribe('pty-1', 'phone-A', { cols: 45, rows: 20 })
|
||||
@@ -291,6 +345,37 @@ describe('mobile presence lock — driver state machine', () => {
|
||||
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'idle' })
|
||||
})
|
||||
|
||||
it.each(['input', 'legacy'] as const)(
|
||||
'a %s phone-fit completion after disconnect cannot recreate the mobile lock',
|
||||
async (entry) => {
|
||||
const { runtime, driverEvents } = createRuntime()
|
||||
await runtime.handleMobileSubscribe('pty-1', 'phone-A', { cols: 45, rows: 20 })
|
||||
await runtime.reclaimTerminalForDesktop('pty-1')
|
||||
|
||||
let finishLayout!: () => void
|
||||
vi.spyOn(runtime, 'applyMobileDisplayMode').mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
finishLayout = () => resolve(true)
|
||||
})
|
||||
)
|
||||
const committing =
|
||||
entry === 'input'
|
||||
? runtime.beginMobileInputFloor('pty-1', 'phone-A')!.commit()
|
||||
: runtime.mobileTookFloor('pty-1', 'phone-A')
|
||||
runtime.handleMobileUnsubscribe('pty-1', 'phone-A')
|
||||
await vi.advanceTimersByTimeAsync(250)
|
||||
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'idle' })
|
||||
|
||||
finishLayout()
|
||||
await committing
|
||||
await vi.advanceTimersByTimeAsync(10 * 60_000)
|
||||
|
||||
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'idle' })
|
||||
expect(driverEvents.at(-1)?.driver).toEqual({ kind: 'idle' })
|
||||
}
|
||||
)
|
||||
|
||||
it('admits a soft-leaving client to reserve the input floor within the grace window', async () => {
|
||||
const { runtime } = createRuntime()
|
||||
await runtime.handleMobileSubscribe('pty-1', 'phone-A', { cols: 45, rows: 20 })
|
||||
|
||||
@@ -37,6 +37,10 @@ export class RuntimeTerminalDriverController {
|
||||
}
|
||||
|
||||
set(ptyId: string, next: DriverState): void {
|
||||
// A desktop reclaim or disconnect supersedes unfinished mobile input.
|
||||
if (next.kind !== 'mobile') {
|
||||
this.inputFloorClaims.delete(ptyId)
|
||||
}
|
||||
const prev = this.get(ptyId)
|
||||
if (prev.kind === next.kind) {
|
||||
if (prev.kind === 'mobile' && next.kind === 'mobile' && prev.clientId === next.clientId) {
|
||||
@@ -59,6 +63,7 @@ export class RuntimeTerminalDriverController {
|
||||
}
|
||||
|
||||
clear(ptyId: string): boolean {
|
||||
this.inputFloorClaims.delete(ptyId)
|
||||
if (!this.current.delete(ptyId)) {
|
||||
return false
|
||||
}
|
||||
@@ -104,7 +109,7 @@ export class RuntimeTerminalDriverController {
|
||||
}
|
||||
settled = true
|
||||
state.pending.delete(token)
|
||||
if (generation < state.committedGeneration) {
|
||||
if (this.inputFloorClaims.get(ptyId) !== state || generation < state.committedGeneration) {
|
||||
this.deleteSettledClaim(ptyId, state)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -14,8 +14,12 @@ export abstract class OrcaRuntimeWithMobileTookFloor extends OrcaRuntimeWithMark
|
||||
ptyId: string,
|
||||
clientId: string,
|
||||
previousFloor?: DriverState,
|
||||
isCurrent: () => boolean = () => true
|
||||
isCurrent?: () => boolean
|
||||
): Promise<void> {
|
||||
if (!isCurrent) {
|
||||
await this.beginMobileInputFloor(ptyId, clientId)?.commit()
|
||||
return
|
||||
}
|
||||
const inner = this.mobileSubscribers.get(ptyId)
|
||||
const sub = inner?.get(clientId)
|
||||
const softLeaver = this.pendingSoftLeavers.get(ptyId)
|
||||
@@ -39,9 +43,12 @@ export abstract class OrcaRuntimeWithMobileTookFloor extends OrcaRuntimeWithMark
|
||||
}
|
||||
await this.applyMobileDisplayMode(ptyId)
|
||||
}
|
||||
// Why: display changes are async; a later PTY write must keep the floor
|
||||
// when an older phone-fit operation eventually completes.
|
||||
if (!isCurrent()) {
|
||||
// Newer input or disconnect can supersede an asynchronous phone-fit.
|
||||
if (
|
||||
!isCurrent() ||
|
||||
(!this.mobileSubscribers.get(ptyId)?.has(clientId) &&
|
||||
this.pendingSoftLeavers.get(ptyId)?.clientId !== clientId)
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.setDriver(ptyId, { kind: 'mobile', clientId })
|
||||
|
||||
Reference in New Issue
Block a user