mirror of
https://github.com/stablyai/orca.git
synced 2026-10-06 16:02:25 +00:00
Fix managed Codex account switching on shared CODEX_HOME (#4862)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -724,6 +724,328 @@ describe('CodexRuntimeHomeService', () => {
|
||||
expect(existsSync(getRuntimeCodexHomePath())).toBe(true)
|
||||
})
|
||||
|
||||
it('uses the same host CODEX_HOME after switching managed Codex accounts', async () => {
|
||||
const runtimeAuthPath = getRuntimeCodexAuthPath()
|
||||
const account1Auth = createCodexAuthJson('one@example.com', 'acct-1', 'one')
|
||||
const account2Auth = createCodexAuthJson('two@example.com', 'acct-2', 'two')
|
||||
const managedHomePath1 = createManagedAuth(testState.userDataDir, 'account-1', account1Auth)
|
||||
const managedHomePath2 = createManagedAuth(testState.userDataDir, 'account-2', account2Auth)
|
||||
const settings = createSettings({
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'one@example.com',
|
||||
managedHomePath: managedHomePath1,
|
||||
providerAccountId: 'acct-1',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-1',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
},
|
||||
{
|
||||
id: 'account-2',
|
||||
email: 'two@example.com',
|
||||
managedHomePath: managedHomePath2,
|
||||
providerAccountId: 'acct-2',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-2',
|
||||
createdAt: 2,
|
||||
updatedAt: 2,
|
||||
lastAuthenticatedAt: 2
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: 'account-1',
|
||||
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
|
||||
})
|
||||
const store = createStore(settings)
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
const account1Home = service.prepareForCodexLaunch()
|
||||
settings.activeCodexManagedAccountId = 'account-2'
|
||||
settings.activeCodexManagedAccountIdsByRuntime = { host: 'account-2', wsl: {} }
|
||||
const account2Home = service.prepareForCodexLaunch()
|
||||
|
||||
expect(account1Home).toBe(getRuntimeCodexHomePath())
|
||||
expect(account2Home).toBe(getRuntimeCodexHomePath())
|
||||
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(account2Auth)
|
||||
})
|
||||
|
||||
it('new host Codex launches get the selected account after preserving outgoing refreshes', async () => {
|
||||
const runtimeAuthPath = getRuntimeCodexAuthPath()
|
||||
const account1Auth = createCodexAuthJson('one@example.com', 'acct-1', 'one', 1)
|
||||
const account1RefreshedAuth = createCodexAuthJson(
|
||||
'one@example.com',
|
||||
'acct-1',
|
||||
'one-refreshed',
|
||||
2
|
||||
)
|
||||
const account2Auth = createCodexAuthJson('two@example.com', 'acct-2', 'two', 1)
|
||||
const managedHomePath1 = createManagedAuth(testState.userDataDir, 'account-1', account1Auth)
|
||||
const managedHomePath2 = createManagedAuth(testState.userDataDir, 'account-2', account2Auth)
|
||||
const settings = createSettings({
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'one@example.com',
|
||||
managedHomePath: managedHomePath1,
|
||||
providerAccountId: 'acct-1',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-1',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
},
|
||||
{
|
||||
id: 'account-2',
|
||||
email: 'two@example.com',
|
||||
managedHomePath: managedHomePath2,
|
||||
providerAccountId: 'acct-2',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-2',
|
||||
createdAt: 2,
|
||||
updatedAt: 2,
|
||||
lastAuthenticatedAt: 2
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: 'account-1',
|
||||
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
|
||||
})
|
||||
const store = createStore(settings)
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
writeFileSync(runtimeAuthPath, account1RefreshedAuth, 'utf-8')
|
||||
settings.activeCodexManagedAccountId = 'account-2'
|
||||
settings.activeCodexManagedAccountIdsByRuntime = { host: 'account-2', wsl: {} }
|
||||
|
||||
expect(service.prepareForCodexLaunch()).toBe(getRuntimeCodexHomePath())
|
||||
expect(readFileSync(join(managedHomePath1, 'auth.json'), 'utf-8')).toBe(account1RefreshedAuth)
|
||||
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(account2Auth)
|
||||
})
|
||||
|
||||
it('reads back selected-account refreshes without ambiguity from duplicate identities', async () => {
|
||||
const runtimeAuthPath = getRuntimeCodexAuthPath()
|
||||
const account1Auth = createCodexAuthJson('same@example.com', 'acct-same', 'one', 1)
|
||||
const account1RefreshedAuth = createCodexAuthJson(
|
||||
'same@example.com',
|
||||
'acct-same',
|
||||
'one-refreshed',
|
||||
2
|
||||
)
|
||||
const account2Auth = createCodexAuthJson('same@example.com', 'acct-same', 'two', 1)
|
||||
const managedHomePath1 = createManagedAuth(testState.userDataDir, 'account-1', account1Auth)
|
||||
const managedHomePath2 = createManagedAuth(testState.userDataDir, 'account-2', account2Auth)
|
||||
const settings = createSettings({
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'same@example.com',
|
||||
managedHomePath: managedHomePath1,
|
||||
providerAccountId: 'acct-same',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-same',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
},
|
||||
{
|
||||
id: 'account-2',
|
||||
email: 'same@example.com',
|
||||
managedHomePath: managedHomePath2,
|
||||
providerAccountId: 'acct-same',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-same',
|
||||
createdAt: 2,
|
||||
updatedAt: 2,
|
||||
lastAuthenticatedAt: 2
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: 'account-1',
|
||||
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
|
||||
})
|
||||
const store = createStore(settings)
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
writeFileSync(runtimeAuthPath, account1RefreshedAuth, 'utf-8')
|
||||
service.syncForCurrentSelection()
|
||||
|
||||
expect(readFileSync(join(managedHomePath1, 'auth.json'), 'utf-8')).toBe(account1RefreshedAuth)
|
||||
expect(readFileSync(join(managedHomePath2, 'auth.json'), 'utf-8')).toBe(account2Auth)
|
||||
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(account1RefreshedAuth)
|
||||
})
|
||||
|
||||
it('keeps fresher selected-account startup refreshes when duplicate identities exist', async () => {
|
||||
const runtimeAuthPath = getRuntimeCodexAuthPath()
|
||||
const account1Auth = createCodexAuthJson('same@example.com', 'acct-same', 'one', 1)
|
||||
const account1RefreshedAuth = createCodexAuthJson(
|
||||
'same@example.com',
|
||||
'acct-same',
|
||||
'one-refreshed',
|
||||
2
|
||||
)
|
||||
const account2Auth = createCodexAuthJson('same@example.com', 'acct-same', 'two', 1)
|
||||
const managedHomePath1 = createManagedAuth(testState.userDataDir, 'account-1', account1Auth)
|
||||
const managedHomePath2 = createManagedAuth(testState.userDataDir, 'account-2', account2Auth)
|
||||
const settings = createSettings({
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'same@example.com',
|
||||
managedHomePath: managedHomePath1,
|
||||
providerAccountId: 'acct-same',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-same',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
},
|
||||
{
|
||||
id: 'account-2',
|
||||
email: 'same@example.com',
|
||||
managedHomePath: managedHomePath2,
|
||||
providerAccountId: 'acct-same',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-same',
|
||||
createdAt: 2,
|
||||
updatedAt: 2,
|
||||
lastAuthenticatedAt: 2
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: 'account-1',
|
||||
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
|
||||
})
|
||||
writeFileSync(runtimeAuthPath, account1RefreshedAuth, 'utf-8')
|
||||
const store = createStore(settings)
|
||||
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
new CodexRuntimeHomeService(store as never)
|
||||
|
||||
expect(readFileSync(join(managedHomePath1, 'auth.json'), 'utf-8')).toBe(account1RefreshedAuth)
|
||||
expect(readFileSync(join(managedHomePath2, 'auth.json'), 'utf-8')).toBe(account2Auth)
|
||||
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(account1RefreshedAuth)
|
||||
})
|
||||
|
||||
it('routes stale live-pane startup refreshes to the matching account before restoring selected auth', async () => {
|
||||
const runtimeAuthPath = getRuntimeCodexAuthPath()
|
||||
const account1Auth = createCodexAuthJson('one@example.com', 'acct-1', 'one', 1)
|
||||
const account1RefreshedAuth = createCodexAuthJson(
|
||||
'one@example.com',
|
||||
'acct-1',
|
||||
'one-refreshed',
|
||||
2
|
||||
)
|
||||
const account2Auth = createCodexAuthJson('two@example.com', 'acct-2', 'two', 1)
|
||||
const managedHomePath1 = createManagedAuth(testState.userDataDir, 'account-1', account1Auth)
|
||||
const managedHomePath2 = createManagedAuth(testState.userDataDir, 'account-2', account2Auth)
|
||||
const settings = createSettings({
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'one@example.com',
|
||||
managedHomePath: managedHomePath1,
|
||||
providerAccountId: 'acct-1',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-1',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
},
|
||||
{
|
||||
id: 'account-2',
|
||||
email: 'two@example.com',
|
||||
managedHomePath: managedHomePath2,
|
||||
providerAccountId: 'acct-2',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-2',
|
||||
createdAt: 2,
|
||||
updatedAt: 2,
|
||||
lastAuthenticatedAt: 2
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: 'account-2',
|
||||
activeCodexManagedAccountIdsByRuntime: { host: 'account-2', wsl: {} }
|
||||
})
|
||||
writeFileSync(runtimeAuthPath, account1RefreshedAuth, 'utf-8')
|
||||
const store = createStore(settings)
|
||||
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
new CodexRuntimeHomeService(store as never)
|
||||
|
||||
expect(readFileSync(join(managedHomePath1, 'auth.json'), 'utf-8')).toBe(account1RefreshedAuth)
|
||||
expect(readFileSync(join(managedHomePath2, 'auth.json'), 'utf-8')).toBe(account2Auth)
|
||||
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(account2Auth)
|
||||
})
|
||||
|
||||
it('preserves duplicate-identity outgoing refreshes before switching to another account', async () => {
|
||||
const runtimeAuthPath = getRuntimeCodexAuthPath()
|
||||
const account1Auth = createCodexAuthJson('same@example.com', 'acct-same', 'one', 1)
|
||||
const account1RefreshedAuth = createCodexAuthJson(
|
||||
'same@example.com',
|
||||
'acct-same',
|
||||
'one-refreshed',
|
||||
2
|
||||
)
|
||||
const account2Auth = createCodexAuthJson('same@example.com', 'acct-same', 'two', 1)
|
||||
const account3Auth = createCodexAuthJson('three@example.com', 'acct-3', 'three', 1)
|
||||
const managedHomePath1 = createManagedAuth(testState.userDataDir, 'account-1', account1Auth)
|
||||
const managedHomePath2 = createManagedAuth(testState.userDataDir, 'account-2', account2Auth)
|
||||
const managedHomePath3 = createManagedAuth(testState.userDataDir, 'account-3', account3Auth)
|
||||
const settings = createSettings({
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'same@example.com',
|
||||
managedHomePath: managedHomePath1,
|
||||
providerAccountId: 'acct-same',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-same',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
},
|
||||
{
|
||||
id: 'account-2',
|
||||
email: 'same@example.com',
|
||||
managedHomePath: managedHomePath2,
|
||||
providerAccountId: 'acct-same',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-same',
|
||||
createdAt: 2,
|
||||
updatedAt: 2,
|
||||
lastAuthenticatedAt: 2
|
||||
},
|
||||
{
|
||||
id: 'account-3',
|
||||
email: 'three@example.com',
|
||||
managedHomePath: managedHomePath3,
|
||||
providerAccountId: 'acct-3',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-3',
|
||||
createdAt: 3,
|
||||
updatedAt: 3,
|
||||
lastAuthenticatedAt: 3
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: 'account-1',
|
||||
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
|
||||
})
|
||||
const store = createStore(settings)
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
writeFileSync(runtimeAuthPath, account1RefreshedAuth, 'utf-8')
|
||||
settings.activeCodexManagedAccountId = 'account-3'
|
||||
settings.activeCodexManagedAccountIdsByRuntime = { host: 'account-3', wsl: {} }
|
||||
service.syncForCurrentSelection()
|
||||
|
||||
expect(readFileSync(join(managedHomePath1, 'auth.json'), 'utf-8')).toBe(account1RefreshedAuth)
|
||||
expect(readFileSync(join(managedHomePath2, 'auth.json'), 'utf-8')).toBe(account2Auth)
|
||||
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(account3Auth)
|
||||
})
|
||||
|
||||
it('mirrors later system Codex config changes before launch', async () => {
|
||||
const systemCodexHome = getSystemCodexHomePath()
|
||||
mkdirSync(systemCodexHome, { recursive: true })
|
||||
|
||||
@@ -313,6 +313,20 @@ export class CodexRuntimeHomeService {
|
||||
private readBackRefreshedTokens(options: {
|
||||
updateLastWrittenAuthJson: boolean
|
||||
}): CodexReadBackResult {
|
||||
const selectedAccountId = normalizeCodexRuntimeSelection(this.store.getSettings()).host
|
||||
if (selectedAccountId) {
|
||||
const selectedAccountResult = this.readBackRefreshedTokensFromPath(
|
||||
this.getRuntimeAuthPath(),
|
||||
{
|
||||
...options,
|
||||
expectedAccountId: selectedAccountId
|
||||
}
|
||||
)
|
||||
if (selectedAccountResult !== 'rejected') {
|
||||
return selectedAccountResult
|
||||
}
|
||||
}
|
||||
|
||||
return this.readBackRefreshedTokensFromPath(this.getRuntimeAuthPath(), options)
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,64 @@ describe('markLiveCodexSessionsForRestart', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('marks every live Codex split pane and ignores non-Codex panes', async () => {
|
||||
useAppStore.setState({
|
||||
tabsByWorktree: {
|
||||
wt1: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
ptyId: 'pty-1',
|
||||
worktreeId: 'wt1',
|
||||
title: 'orca-1',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
},
|
||||
{
|
||||
id: 'tab-2',
|
||||
ptyId: 'pty-3',
|
||||
worktreeId: 'wt1',
|
||||
title: 'orca-2',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 1,
|
||||
createdAt: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
ptyIdsByTabId: {
|
||||
'tab-1': ['pty-1', 'pty-2'],
|
||||
'tab-2': ['pty-3']
|
||||
}
|
||||
})
|
||||
vi.mocked(window.api.pty.getForegroundProcess).mockImplementation((ptyId) => {
|
||||
if (ptyId === 'pty-1') {
|
||||
return Promise.resolve('codex')
|
||||
}
|
||||
if (ptyId === 'pty-3') {
|
||||
return Promise.resolve('codex-aarch64-ap')
|
||||
}
|
||||
return Promise.resolve('zsh')
|
||||
})
|
||||
|
||||
await markLiveCodexSessionsForRestart({
|
||||
previousAccountLabel: ACCOUNT_A,
|
||||
nextAccountLabel: ACCOUNT_B
|
||||
})
|
||||
|
||||
expect(useAppStore.getState().codexRestartNoticeByPtyId).toEqual({
|
||||
'pty-1': {
|
||||
previousAccountLabel: ACCOUNT_A,
|
||||
nextAccountLabel: ACCOUNT_B
|
||||
},
|
||||
'pty-3': {
|
||||
previousAccountLabel: ACCOUNT_A,
|
||||
nextAccountLabel: ACCOUNT_B
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('does not mark non-codex foreground processes', async () => {
|
||||
vi.mocked(window.api.pty.getForegroundProcess).mockResolvedValue('zsh')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user