mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
fix(mobile): preserve reconnect revision evidence
This commit is contained in:
@@ -109,7 +109,7 @@ export default function EditHostScreen() {
|
||||
...(willRename ? { name: nextName } : {}),
|
||||
...(nextEndpoint !== undefined ? { endpoint: nextEndpoint } : {})
|
||||
}
|
||||
let reconnectProfile: HostProfile | null = hostProfileAfterEdit(host, updates)
|
||||
let reconnectProfile: HostProfile | null | undefined = hostProfileAfterEdit(host, updates)
|
||||
|
||||
savingRef.current = true
|
||||
setSaving(true)
|
||||
@@ -132,8 +132,13 @@ export default function EditHostScreen() {
|
||||
// elsewhere in the app picks up the fresh state regardless.
|
||||
const hostLoadRevision = getHostListLoadRevision()
|
||||
const hosts = await loadHosts()
|
||||
primeHosts(hosts, hostLoadRevision)
|
||||
reconnectProfile = hosts.find(({ id }) => id === host.id) ?? null
|
||||
if (hostLoadRevision === getHostListLoadRevision()) {
|
||||
primeHosts(hosts, hostLoadRevision)
|
||||
reconnectProfile = hosts.find(({ id }) => id === host.id) ?? null
|
||||
} else {
|
||||
// A concurrent publication already primed a newer reconnect profile.
|
||||
reconnectProfile = undefined
|
||||
}
|
||||
} catch {
|
||||
// best-effort re-prime; persisted data is unaffected
|
||||
}
|
||||
@@ -142,7 +147,7 @@ export default function EditHostScreen() {
|
||||
setSaving(false)
|
||||
router.back()
|
||||
|
||||
if (nextEndpoint !== undefined && reconnectProfile) {
|
||||
if (nextEndpoint !== undefined && reconnectProfile !== null) {
|
||||
// Why: reconnect is a follow-on side effect of a save that already
|
||||
// committed — its failure or a hang must not be reported as a save
|
||||
// failure or block navigating back.
|
||||
|
||||
@@ -7,6 +7,7 @@ const dependencies = vi.hoisted(() => ({
|
||||
alert: vi.fn(),
|
||||
back: vi.fn(),
|
||||
forceReconnectHost: vi.fn(),
|
||||
getHostListLoadRevision: vi.fn(() => 0),
|
||||
loadHosts: vi.fn(),
|
||||
primeHosts: vi.fn(),
|
||||
updateHostNameAndEndpoint: vi.fn(),
|
||||
@@ -44,6 +45,10 @@ vi.mock('./transport/host-store', () => ({
|
||||
updateHostNameAndEndpoint: dependencies.updateHostNameAndEndpoint
|
||||
}))
|
||||
|
||||
vi.mock('./transport/host-list-load-sharing', () => ({
|
||||
getHostListLoadRevision: dependencies.getHostListLoadRevision
|
||||
}))
|
||||
|
||||
vi.mock('./transport/client-context', () => ({
|
||||
useForceReconnect: () => dependencies.forceReconnectHost,
|
||||
usePrimeHosts: () => dependencies.primeHosts
|
||||
@@ -142,6 +147,7 @@ describe('edit host handleSave', () => {
|
||||
dependencies.alert.mockReset()
|
||||
dependencies.back.mockReset()
|
||||
dependencies.forceReconnectHost.mockReset().mockResolvedValue(undefined)
|
||||
dependencies.getHostListLoadRevision.mockReset().mockReturnValue(0)
|
||||
dependencies.loadHosts.mockReset().mockResolvedValue([HOST_FIXTURE])
|
||||
dependencies.primeHosts.mockReset()
|
||||
dependencies.updateHostNameAndEndpoint.mockReset().mockResolvedValue(undefined)
|
||||
@@ -285,6 +291,20 @@ describe('edit host handleSave', () => {
|
||||
act(() => renderer.unmount())
|
||||
})
|
||||
|
||||
it('uses the newer cached profile when the persisted snapshot is invalidated mid-load', async () => {
|
||||
const staleHost = { ...HOST_FIXTURE, endpoint: 'ws://192.168.1.20:6768' }
|
||||
dependencies.loadHosts.mockResolvedValueOnce([HOST_FIXTURE]).mockResolvedValueOnce([staleHost])
|
||||
dependencies.getHostListLoadRevision.mockReturnValueOnce(1).mockReturnValueOnce(2)
|
||||
const renderer = await renderEditHostRoute()
|
||||
setFieldValue(renderer, 'Address', '192.168.1.20:6768')
|
||||
await pressSave(renderer)
|
||||
|
||||
expect(dependencies.primeHosts).not.toHaveBeenCalled()
|
||||
expect(dependencies.forceReconnectHost).toHaveBeenCalledWith('host-1', undefined)
|
||||
|
||||
act(() => renderer.unmount())
|
||||
})
|
||||
|
||||
it('ignores a second Save trigger while a save is already in flight', async () => {
|
||||
let resolveSave: () => void = () => {}
|
||||
dependencies.updateHostNameAndEndpoint.mockImplementationOnce(
|
||||
@@ -322,6 +342,7 @@ describe('edit host load() error states', () => {
|
||||
dependencies.alert.mockReset()
|
||||
dependencies.back.mockReset()
|
||||
dependencies.forceReconnectHost.mockReset().mockResolvedValue(undefined)
|
||||
dependencies.getHostListLoadRevision.mockReset().mockReturnValue(0)
|
||||
dependencies.loadHosts.mockReset().mockResolvedValue([HOST_FIXTURE])
|
||||
dependencies.primeHosts.mockReset()
|
||||
dependencies.updateHostNameAndEndpoint.mockReset().mockResolvedValue(undefined)
|
||||
|
||||
@@ -356,6 +356,34 @@ describe('mobile rpc-client request deadline', () => {
|
||||
client.close()
|
||||
})
|
||||
|
||||
it('keeps late activity-probe evidence across a queued probe handoff', async () => {
|
||||
const client = connect('ws://desktop.invalid', 'token', 'server-key')
|
||||
const socket = mockSockets[0]!
|
||||
socket.open()
|
||||
client.notifyForeground()
|
||||
const firstProbe = sentRequest(socket, 'status.get')
|
||||
const stalled = client.sendRequest('browser.screenshot', {}, { timeoutMs: 100 })
|
||||
const stalledOutcome = stalled.catch((error: unknown) => error)
|
||||
const healthy = client.sendRequest('speech.models.list', {}, { timeoutMs: 100 })
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
const healthyRequest = sentRequest(socket, 'speech.models.list')
|
||||
socket.receive(`encrypted:${JSON.stringify({ id: healthyRequest.id, ok: true, result: {} })}`)
|
||||
|
||||
await expect(healthy).resolves.toMatchObject({ ok: true })
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
await expect(stalledOutcome).resolves.toMatchObject({
|
||||
message: 'Request timed out: browser.screenshot'
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(7_900)
|
||||
expect(sentRequests(socket, 'status.get')).toHaveLength(2)
|
||||
socket.receive(`encrypted:${JSON.stringify({ id: firstProbe.id, ok: true, result: {} })}`)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(8_000)
|
||||
expect(client.getState()).toBe('connected')
|
||||
expect(socket.close).not.toHaveBeenCalled()
|
||||
client.close()
|
||||
})
|
||||
|
||||
it('keeps Force Reconnect inside its deadline after a late connection', async () => {
|
||||
const client = connect('ws://desktop.invalid', 'token', 'server-key')
|
||||
const verification = verifyForceReconnectRpcHealth(client)
|
||||
|
||||
@@ -798,6 +798,7 @@ export function connect(
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true
|
||||
pending.delete(id)
|
||||
timedOutControlRequestIds.add(id)
|
||||
const controlResponded = controlResponseSequence > probeControlResponseSequence
|
||||
activityProbeFollowUp.finish(probeWs)
|
||||
if (controlResponded) {
|
||||
|
||||
Reference in New Issue
Block a user