mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 00:02:56 +00:00
fix(relay): stop a mid-read profile switch reporting as a sign-out
Same taxonomy error8972d744fixed for the session file, in the other null exit. readRelayAuthContext re-reads the active profile after the refresh, because refresh and org selection can rewrite cloud linkage in flight. It then collapsed two unrelated outcomes into null: if (!cloud || refreshed.profile.id !== active.profile.id) return null A profile switch landing inside that window is not "the cloud session is gone", which is the contract the coordinator's own comment says null carries. Measured before: offlineReason "signed-out" — the terminal wire reason every paired phone latches as "sign in on your desktop to reconnect", arming no retry — for a race the switch's own authMutated() re-reads moments later. After: auth_unavailable, which is retryable. Split the condition: throw for the id mismatch, keep null for a profile that genuinely has no cloud linkage. Still unfixed, and not reachable from here: ensureActiveOrcaProfile treats an unreadable profile index the same as a corrupt one — readProfileIndexFile catches every error including EACCES/EMFILE, index and .bak fail together under descriptor exhaustion, and it fabricates a default local profile and writes it over the unreadable file. readRelayAuthContext then sees no cloud and reports a sign-out honestly, because by that point the index really has been replaced. That one belongs in profile-index-store.ts and changes app-wide sign-in semantics, the same caveat8972d744raised for decrypt-failed.
This commit is contained in:
@@ -24,8 +24,15 @@ export async function readRelayAuthContext(
|
||||
// Why: refresh and org-selection can rewrite cloud linkage while the request
|
||||
// is in flight; identity must come from the post-refresh profile state.
|
||||
const refreshed = ensureActiveOrcaProfile(userDataPath)
|
||||
// Why throw rather than return null, same argument as the unreadable session above:
|
||||
// a profile switch landing inside this read is not a sign-out, and null spends the
|
||||
// terminal SIGNED_OUT — latched by every paired phone, arming no retry — on a race
|
||||
// the switch's own auth mutation re-reads moments later.
|
||||
if (refreshed.profile.id !== active.profile.id) {
|
||||
throw new Error('orca_profile_switched_during_read')
|
||||
}
|
||||
const cloud = refreshed.profile.cloud
|
||||
if (!cloud || refreshed.profile.id !== active.profile.id) {
|
||||
if (!cloud) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { RELAY_HOST_CLOSE_REASON } from '../../../shared/relay-host-close-reason'
|
||||
|
||||
// Same taxonomy question 8972d744 answered for the session file, asked of the
|
||||
// other null exit: readRelayAuthContext re-reads the profile after the refresh,
|
||||
// and a profile switch landing in that window is not a sign-out.
|
||||
const fakes = vi.hoisted(() => ({
|
||||
ensureActiveOrcaProfile: vi.fn(),
|
||||
readFreshOrcaCloudSession: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../../orca-profiles/profile-index-store', () => ({
|
||||
ensureActiveOrcaProfile: fakes.ensureActiveOrcaProfile
|
||||
}))
|
||||
vi.mock('../../orca-profiles/profile-cloud-session-refresh', () => ({
|
||||
readFreshOrcaCloudSession: fakes.readFreshOrcaCloudSession
|
||||
}))
|
||||
|
||||
import { readRelayAuthContext } from './relay-auth-context'
|
||||
import { RelayAuthCoordinator } from './relay-auth-coordinator'
|
||||
|
||||
const authConfig = {} as never
|
||||
|
||||
function profile(id: string, cloud: object | null) {
|
||||
return { profile: { id, ...(cloud ? { cloud } : {}) } }
|
||||
}
|
||||
|
||||
const signedIn = { userId: 'u1', cloudProfileId: 'cp1', activeOrgId: 'org-1' }
|
||||
|
||||
function foundSession() {
|
||||
return {
|
||||
status: 'found',
|
||||
session: { accessToken: 'access-1', capabilities: { flags: { 'relay.use': true } } }
|
||||
}
|
||||
}
|
||||
|
||||
describe('readRelayAuthContext profile-switch race', () => {
|
||||
it('refuses to call a mid-read profile switch a sign-out', async () => {
|
||||
// ensureActiveOrcaProfile is called twice — once before the refresh and once
|
||||
// after, because refresh and org selection can rewrite cloud linkage. A
|
||||
// switch between them means "re-read", not "the cloud session is gone".
|
||||
fakes.ensureActiveOrcaProfile
|
||||
.mockReturnValueOnce(profile('profile-1', signedIn))
|
||||
.mockReturnValueOnce(profile('profile-2', signedIn))
|
||||
fakes.readFreshOrcaCloudSession.mockResolvedValue(foundSession())
|
||||
|
||||
await expect(readRelayAuthContext(authConfig, '/tmp/x')).rejects.toThrow(
|
||||
'orca_profile_switched_during_read'
|
||||
)
|
||||
})
|
||||
|
||||
it('classifies the switch as auth_unavailable, never signed_out', async () => {
|
||||
fakes.ensureActiveOrcaProfile
|
||||
.mockReturnValueOnce(profile('profile-1', signedIn))
|
||||
.mockReturnValueOnce(profile('profile-2', signedIn))
|
||||
fakes.readFreshOrcaCloudSession.mockResolvedValue(foundSession())
|
||||
const broker = { closeNow: vi.fn() }
|
||||
const coordinator = new RelayAuthCoordinator({
|
||||
readContext: () => readRelayAuthContext(authConfig, '/tmp/x'),
|
||||
openBroker: async () => broker,
|
||||
onStatus: vi.fn()
|
||||
})
|
||||
coordinator.reconcile()
|
||||
|
||||
const result = await coordinator.waitForLiveBrokerResult(0)
|
||||
expect(result).toEqual({ broker: null, offlineReason: 'auth_unavailable' })
|
||||
// SIGNED_OUT is terminal on the wire — the phone latches it and arms no retry.
|
||||
expect(broker.closeNow).not.toHaveBeenCalledWith(RELAY_HOST_CLOSE_REASON.SIGNED_OUT)
|
||||
})
|
||||
|
||||
it('still reports a profile that genuinely has no cloud linkage as gone', async () => {
|
||||
fakes.ensureActiveOrcaProfile
|
||||
.mockReturnValueOnce(profile('profile-1', signedIn))
|
||||
.mockReturnValueOnce(profile('profile-1', null))
|
||||
fakes.readFreshOrcaCloudSession.mockResolvedValue(foundSession())
|
||||
|
||||
await expect(readRelayAuthContext(authConfig, '/tmp/x')).resolves.toBeNull()
|
||||
})
|
||||
|
||||
it('returns the post-refresh identity when the profile held still', async () => {
|
||||
fakes.ensureActiveOrcaProfile
|
||||
.mockReturnValueOnce(profile('profile-1', signedIn))
|
||||
.mockReturnValueOnce(profile('profile-1', { ...signedIn, activeOrgId: 'org-2' }))
|
||||
fakes.readFreshOrcaCloudSession.mockResolvedValue(foundSession())
|
||||
|
||||
await expect(readRelayAuthContext(authConfig, '/tmp/x')).resolves.toEqual({
|
||||
identity: { userId: 'u1', profileId: 'cp1', organizationId: 'org-2' },
|
||||
accessToken: 'access-1',
|
||||
relayEntitled: true
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user