mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Hot-swap Codex homes via active pointer
Route Codex terminal launches through stable active CODEX_HOME pointers for host and WSL launch homes, with account-switch repointing and validated Windows/WSL behavior.
This commit is contained in:
@@ -4,9 +4,9 @@
|
||||
|
||||
Orca stopped launching Codex from the user's global `~/.codex` because global hook and config mutations were intrusive. The current Orca-owned host runtime home is shared across accounts:
|
||||
|
||||
- [src/main/codex-accounts/runtime-home-service.ts](../src/main/codex-accounts/runtime-home-service.ts:105) prepares launch and rate-limit homes.
|
||||
- [src/main/codex-accounts/runtime-home-service.ts](../src/main/codex-accounts/runtime-home-service.ts:146) materializes the active account by writing `auth.json` into the shared runtime home.
|
||||
- [src/main/ipc/pty.ts](../src/main/ipc/pty.ts:608) injects the selected `CODEX_HOME` into new PTYs.
|
||||
- [src/main/codex-accounts/runtime-home-service.ts](../src/main/codex-accounts/runtime-home-service.ts:119) prepares launch and rate-limit homes.
|
||||
- [src/main/codex-accounts/runtime-home-service.ts](../src/main/codex-accounts/runtime-home-service.ts:1093) materializes an active pointer for terminal launches.
|
||||
- [src/main/ipc/pty.ts](../src/main/ipc/pty.ts:608) injects the selected `CODEX_HOME` into PTYs.
|
||||
- [src/renderer/src/lib/codex-session-restart.ts](../src/renderer/src/lib/codex-session-restart.ts:24) marks only currently foreground Codex processes for restart after account switches.
|
||||
|
||||
PR #1629 fixed stored credential clobbering by verifying identity before read-back. That prevents Account A tokens from being saved into Account B's managed account. It does not remove the live-process race where Account A can still write the shared runtime `auth.json` after the user selects Account B, and a later launch can observe Account A before Orca re-syncs.
|
||||
@@ -22,7 +22,7 @@ Implementation should isolate only `auth.json` by selected account while preserv
|
||||
- Native `~/.codex` is user-owned. Orca may read/copy from it, but Orca-owned hooks and runtime config live in Orca userData.
|
||||
- `codex-runtime-home/home` is the single shared Orca Codex environment.
|
||||
- Host launch homes may contain a real selected `auth.json`. Known shared Codex entries resolve to the shared environment or are reconciled back into it before another selected launch home is prepared.
|
||||
- Account switching cannot mutate the process environment of already-open PTYs. Existing host PTYs that were opened under the old account are stale until restarted, even if no Codex process is currently foregrounded.
|
||||
- Terminal `CODEX_HOME` points at a stable active home. Account switching repoints that active home to the selected launch home so the next `codex` command in an existing shell resolves to the current account.
|
||||
|
||||
## Non-goals
|
||||
|
||||
@@ -62,9 +62,11 @@ Implementation should isolate only `auth.json` by selected account while preserv
|
||||
|
||||
Read-mostly resource entries may use marker-owned copy fallback when links fail. Markers must let Orca refresh/remove only entries it created.
|
||||
|
||||
5. Do not launch new host Codex sessions from the shared environment home.
|
||||
5. Launch terminals through a stable active home.
|
||||
|
||||
`prepareForCodexLaunch()` and rate-limit fetches return the selected launch home. Old sessions that already point at `codex-runtime-home/home` can continue to exist, but fresh launches no longer share their `auth.json` path. Read-back for refreshed tokens is launch-home scoped; `codex-runtime-home/home/auth.json` is ignored for deciding the active account of fresh host launches.
|
||||
`prepareForCodexLaunch()` returns `codex-runtime-home/active/host/home`, which is an Orca-owned directory symlink/junction to the selected launch home. Account switches atomically repoint this active home. New terminals and existing idle shells therefore keep the same `CODEX_HOME` string while the next `codex` process follows the new selected account.
|
||||
|
||||
Rate-limit fetches continue to use the concrete selected launch home so quota reads are not coupled to a mutable pointer. Old sessions that already point at `codex-runtime-home/home` can continue to exist, but fresh launches no longer share their `auth.json` path. Read-back for refreshed tokens is launch-home scoped; `codex-runtime-home/home/auth.json` is ignored for deciding the active account of fresh host launches.
|
||||
|
||||
6. Keep session and usage aggregation shared.
|
||||
|
||||
@@ -74,9 +76,9 @@ Implementation should isolate only `auth.json` by selected account while preserv
|
||||
|
||||
On Windows, directory links use junctions when possible and file links may fail without Developer Mode. Mutable file fallback therefore requires reconciliation; mutable directory fallback must not silently copy. On macOS/Linux, symlinks should work. The fallback path must preserve behavior, not just tests.
|
||||
|
||||
8. Keep WSL behavior stable in this change.
|
||||
8. Mirror the active-home contract for WSL.
|
||||
|
||||
Existing WSL runtime homes are already per-distro and selected by target. This change does not solve same-distro WSL multi-account stale-auth races; that remains a residual risk. Tests should prove host Windows WSL path stripping still works and host launch homes do not leak into WSL.
|
||||
WSL launch homes remain per-distro and selected by target. Windows prepares `codex-runtime-home/active/wsl/home` inside the target distro and points it at that distro's selected launch home. WSL terminals receive the Linux path for the active home, not a Windows host path.
|
||||
|
||||
9. Clean up launch-home credentials.
|
||||
|
||||
@@ -88,6 +90,7 @@ Implementation should isolate only `auth.json` by selected account while preserv
|
||||
- Persist selected account id in settings.
|
||||
- Read back refreshed tokens from the previous selected launch auth path only if identity matches. As a compatibility fallback, a matching old shared-home refresh may be persisted to the outgoing account, but never to the incoming account.
|
||||
- Prepare selected launch home.
|
||||
- Repoint the active home to the selected launch home.
|
||||
- Rate-limit fetch runs against selected launch home.
|
||||
|
||||
- New Codex terminal:
|
||||
@@ -95,17 +98,18 @@ Implementation should isolate only `auth.json` by selected account while preserv
|
||||
- Host target calls `prepareForCodexLaunch()`.
|
||||
- Shared environment home is synced.
|
||||
- Selected launch home is materialized.
|
||||
- `CODEX_HOME` and `ORCA_CODEX_HOME` point to selected launch home.
|
||||
- Active home points to selected launch home.
|
||||
- `CODEX_HOME` and `ORCA_CODEX_HOME` point to active home.
|
||||
|
||||
- Old live Codex process:
|
||||
- Continues writing whichever home it launched from.
|
||||
- Continues writing whichever real home Codex canonicalized at startup.
|
||||
- If it was launched before this change from the shared home, the read-back guard still prevents managed-account corruption.
|
||||
- Fresh launches do not read the old process's shared `auth.json`.
|
||||
|
||||
- Old idle shell:
|
||||
- Keeps the `CODEX_HOME` environment it was spawned with.
|
||||
- If the user later runs `codex` inside that shell, it can still use the old account.
|
||||
- The UI must mark affected host terminal sessions stale or clearly limit the guarantee to fresh PTYs.
|
||||
- Keeps the stable active-home `CODEX_HOME` environment it was spawned with.
|
||||
- If the user later runs `codex` inside that shell, it follows the repointed active home and uses the current account.
|
||||
- Pre-change shells that still point directly at a concrete launch home or the old shared home remain outside this guarantee.
|
||||
|
||||
```text
|
||||
native ~/.codex
|
||||
@@ -114,6 +118,9 @@ native ~/.codex
|
||||
Orca/codex-runtime-home/home
|
||||
shared config.toml, hooks.json, sessions, resources
|
||||
|
||||
Orca/codex-runtime-home/active/host/home
|
||||
symlink/junction to the selected launch home
|
||||
|
||||
Orca/codex-runtime-home/launch/host/account-a/home
|
||||
auth.json real file for account A
|
||||
config.toml link/copy to shared home
|
||||
@@ -134,12 +141,13 @@ Orca/codex-runtime-home/launch/host/account-a/home
|
||||
- A shared config/resource entry is deleted after a launch-home link or fallback copy exists.
|
||||
- Launch-home fallback copy exists but the user edited it manually.
|
||||
- Codex creates a new top-level file in a launch home that Orca does not know is shared state.
|
||||
- Daemon reattach points at an old PTY with old `CODEX_HOME`.
|
||||
- Idle shell opened as Account A later runs Codex after switching to Account B.
|
||||
- Daemon reattach points at a pre-change PTY with a concrete old `CODEX_HOME`.
|
||||
- Pre-change idle shell opened as Account A later runs Codex after switching to Account B.
|
||||
- WSL shell launched from Windows must not receive a host launch-home path.
|
||||
- macOS/Linux symlinks must use relative/absolute targets without Windows junction behavior.
|
||||
- Codex atomically rewrites `config.toml` over a launch-home symlink or fallback copy.
|
||||
- Account removal leaves copied launch-home credentials behind.
|
||||
- Active-home symlink/junction replacement fails and Orca falls back to a concrete selected launch home for that launch.
|
||||
|
||||
## Test Plan
|
||||
|
||||
@@ -154,9 +162,11 @@ Orca/codex-runtime-home/launch/host/account-a/home
|
||||
- Unit: account removal deletes the marked account launch home auth.
|
||||
- Unit: removing an account that never launched does not create a new empty launch-home directory.
|
||||
- Unit: WSL target behavior and Windows WSL path stripping remain unchanged.
|
||||
- Unit: host and WSL `prepareForCodexLaunch()` return active homes whose links target the selected launch homes.
|
||||
- Manual: existing shell with stable `CODEX_HOME` starts a new Codex process against the newly selected account after active-home repoint.
|
||||
- Typecheck: `pnpm run tc:node`, `pnpm run tc:cli`, `pnpm run tc:web`.
|
||||
- Lint: `pnpm run lint`.
|
||||
- Electron validation: launch Orca dev on Windows, create fake managed Codex account state through IPC/store where possible, create a terminal, verify visible terminal exists, and verify backing PTY env/session points at a selected launch home. Capture account settings/status bar and terminal screenshots. On macOS, validate through CI/subagent or targeted path/link tests where local hardware is unavailable.
|
||||
- Electron validation: launch Orca dev on Windows, create fake managed Codex account state through IPC/store where possible, create a terminal, verify visible terminal exists, and verify backing PTY env/session points at an active home that targets the selected launch home. Capture account settings/status bar and terminal screenshots. On macOS, validate through CI/subagent or targeted path/link tests where local hardware is unavailable.
|
||||
|
||||
## UI Quality Bar
|
||||
|
||||
@@ -172,7 +182,7 @@ No intentional UI change. Existing account switcher and terminal restart prompt
|
||||
|
||||
1. Add path helpers for shared environment home and selected host launch homes.
|
||||
2. Add launch-home materialization with link/copy fallback and owned markers.
|
||||
3. Route host `prepareForCodexLaunch()` and `prepareForRateLimitFetch()` to selected launch homes.
|
||||
3. Route host `prepareForCodexLaunch()` through active homes and keep `prepareForRateLimitFetch()` on selected launch homes.
|
||||
4. Keep existing read-back guard but make it read from the relevant selected launch auth path when possible.
|
||||
5. Add regression tests for stale auth, shared non-auth entries, fallback copies, and WSL no-regression.
|
||||
6. Fix the current CLI typecheck include for `codex-config-sync-state.ts`.
|
||||
@@ -202,7 +212,7 @@ No intentional UI change. Existing account switcher and terminal restart prompt
|
||||
3. Terminal after selected-account launch
|
||||
- Residual risks:
|
||||
- A pre-change live Codex process launched from the old shared home can still mutate the old shared `auth.json`; fresh launches should no longer consume it.
|
||||
- A pre-switch idle shell cannot have its process environment changed; it must be restarted or marked stale.
|
||||
- Same-distro WSL launch homes remain out of scope for this host-focused change.
|
||||
- A pre-change idle shell that already has a concrete old `CODEX_HOME` cannot be repaired by repointing the active home.
|
||||
- Windows junction replacement semantics still need native Windows validation; failure falls back to concrete launch homes, preserving correctness but losing hot-swap for that launch.
|
||||
- Unknown Codex-created top-level launch-home files are not adopted into shared state until Orca explicitly classifies them. This avoids crashing or copying locked live sqlite files, but it means "everything except auth" is guaranteed only for the known shared entries above.
|
||||
- If Codex stores account-sensitive data outside `auth.json`, sharing sessions/state may need a later narrower exception.
|
||||
|
||||
@@ -159,10 +159,26 @@ function getSystemLaunchCodexHomePath(): string {
|
||||
return join(testState.userDataDir, 'codex-runtime-home', 'launch', 'host', 'system', 'home')
|
||||
}
|
||||
|
||||
function getActiveHostCodexHomePath(): string {
|
||||
return join(testState.userDataDir, 'codex-runtime-home', 'active', 'host', 'home')
|
||||
}
|
||||
|
||||
function getHostLaunchCodexHomePath(accountId: string | null): string {
|
||||
const segment =
|
||||
accountId === null
|
||||
? 'system'
|
||||
: `account-${createHash('sha256').update(accountId).digest('hex').slice(0, 32)}`
|
||||
return join(testState.userDataDir, 'codex-runtime-home', 'launch', 'host', segment, 'home')
|
||||
}
|
||||
|
||||
function getWslRuntimeCodexHomePath(wslHome: string): string {
|
||||
return join(wslHome, '.local', 'share', 'orca', 'codex-runtime-home', 'home')
|
||||
}
|
||||
|
||||
function getActiveWslCodexHomePath(wslHome: string): string {
|
||||
return join(wslHome, '.local', 'share', 'orca', 'codex-runtime-home', 'active', 'wsl', 'home')
|
||||
}
|
||||
|
||||
function getWslLaunchCodexHomePath(wslHome: string, accountId: string | null): string {
|
||||
const segment =
|
||||
accountId === null
|
||||
@@ -588,15 +604,16 @@ describe('CodexRuntimeHomeService', () => {
|
||||
expect(existsSync(runtimeAuthPath)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns the selected launch home for Codex launch and rate-limit preparation', async () => {
|
||||
it('returns the active home for Codex launch and the selected launch home for rate-limit preparation', async () => {
|
||||
const store = createStore(createSettings())
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
expect(service.prepareForCodexLaunch()).toBe(getSystemLaunchCodexHomePath())
|
||||
expect(service.prepareForCodexLaunch()).toBe(getActiveHostCodexHomePath())
|
||||
expect(service.prepareForRateLimitFetch()).toBe(getSystemLaunchCodexHomePath())
|
||||
expect(existsSync(getRuntimeCodexHomePath())).toBe(true)
|
||||
expect(existsSync(getSystemLaunchCodexHomePath())).toBe(true)
|
||||
expectResourceLinked(getActiveHostCodexHomePath(), getSystemLaunchCodexHomePath())
|
||||
})
|
||||
|
||||
it('uses separate selected host launch homes while sharing non-auth runtime state', async () => {
|
||||
@@ -637,13 +654,16 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
const launchHome1 = service.prepareForCodexLaunch()
|
||||
expect(service.prepareForCodexLaunch()).toBe(getActiveHostCodexHomePath())
|
||||
const launchHome1 = getHostLaunchCodexHomePath('account-1')
|
||||
settings.activeCodexManagedAccountId = 'account-2'
|
||||
settings.activeCodexManagedAccountIdsByRuntime = { host: 'account-2', wsl: {} }
|
||||
const launchHome2 = service.prepareForCodexLaunch()
|
||||
expect(service.prepareForCodexLaunch()).toBe(getActiveHostCodexHomePath())
|
||||
const launchHome2 = getHostLaunchCodexHomePath('account-2')
|
||||
|
||||
expect(launchHome1).not.toBe(launchHome2)
|
||||
expect(launchHome1).toContain(join('codex-runtime-home', 'launch', 'host', 'account-'))
|
||||
expectResourceLinked(getActiveHostCodexHomePath(), launchHome2)
|
||||
expect(readFileSync(join(launchHome1!, 'auth.json'), 'utf-8')).toBe(account1Auth)
|
||||
expect(readFileSync(join(launchHome2!, 'auth.json'), 'utf-8')).toBe(account2Auth)
|
||||
expectResourceLinkedOrCopied(
|
||||
@@ -705,19 +725,22 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
const launchHome1 = service.prepareForCodexLaunch()
|
||||
expect(service.prepareForCodexLaunch()).toBe(getActiveHostCodexHomePath())
|
||||
const launchHome1 = getHostLaunchCodexHomePath('account-1')
|
||||
settings.activeCodexManagedAccountId = 'account-2'
|
||||
settings.activeCodexManagedAccountIdsByRuntime = { host: 'account-2', wsl: {} }
|
||||
const launchHome2 = service.prepareForCodexLaunch()
|
||||
expect(service.prepareForCodexLaunch()).toBe(getActiveHostCodexHomePath())
|
||||
const launchHome2 = getHostLaunchCodexHomePath('account-2')
|
||||
|
||||
expect(readFileSync(join(launchHome1!, 'auth.json'), 'utf-8')).toBe(account1Auth)
|
||||
expect(readFileSync(join(launchHome2!, 'auth.json'), 'utf-8')).toBe(account2Auth)
|
||||
expect(lstatSync(join(launchHome1!, 'auth.json')).isSymbolicLink()).toBe(false)
|
||||
expect(lstatSync(join(launchHome2!, 'auth.json')).isSymbolicLink()).toBe(false)
|
||||
expectResourceLinked(getActiveHostCodexHomePath(), launchHome2)
|
||||
expect(readFileSync(join(launchHome1, 'auth.json'), 'utf-8')).toBe(account1Auth)
|
||||
expect(readFileSync(join(launchHome2, 'auth.json'), 'utf-8')).toBe(account2Auth)
|
||||
expect(lstatSync(join(launchHome1, 'auth.json')).isSymbolicLink()).toBe(false)
|
||||
expect(lstatSync(join(launchHome2, 'auth.json')).isSymbolicLink()).toBe(false)
|
||||
for (const entryName of sqliteEntries) {
|
||||
const sharedPath = join(getRuntimeCodexHomePath(), entryName)
|
||||
expectResourceLinked(join(launchHome1!, entryName), sharedPath)
|
||||
expectResourceLinked(join(launchHome2!, entryName), sharedPath)
|
||||
expectResourceLinked(join(launchHome1, entryName), sharedPath)
|
||||
expectResourceLinked(join(launchHome2, entryName), sharedPath)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1183,10 +1206,12 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslLaunchHomePath = getWslLaunchCodexHomePath(wslHome, 'account-1')
|
||||
|
||||
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe('{"account":"host-system"}\n')
|
||||
expect(service.prepareForCodexLaunch()).toBe(getSystemLaunchCodexHomePath())
|
||||
expect(service.prepareForCodexLaunch()).toBe(getActiveHostCodexHomePath())
|
||||
expectResourceLinked(getActiveHostCodexHomePath(), getSystemLaunchCodexHomePath())
|
||||
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
|
||||
wslLaunchHomePath
|
||||
getActiveWslCodexHomePath(wslHome)
|
||||
)
|
||||
expectResourceLinked(getActiveWslCodexHomePath(wslHome), wslLaunchHomePath)
|
||||
expect(readFileSync(join(wslLaunchHomePath, 'auth.json'), 'utf-8')).toBe(
|
||||
'{"account":"wsl"}\n'
|
||||
)
|
||||
@@ -1251,8 +1276,9 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const wslLaunchHomePath = getWslLaunchCodexHomePath(wslHome, null)
|
||||
|
||||
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
|
||||
wslLaunchHomePath
|
||||
getActiveWslCodexHomePath(wslHome)
|
||||
)
|
||||
expectResourceLinked(getActiveWslCodexHomePath(wslHome), wslLaunchHomePath)
|
||||
expect(store.updateSettings).toHaveBeenCalledWith({
|
||||
activeCodexManagedAccountId: null,
|
||||
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: null } }
|
||||
@@ -1322,7 +1348,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const firstLaunchHomePath = getWslLaunchCodexHomePath(wslHome, 'account-1')
|
||||
const secondLaunchHomePath = getWslLaunchCodexHomePath(wslHome, 'account-2')
|
||||
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(firstLaunchHomePath)
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(getActiveWslCodexHomePath(wslHome))
|
||||
expectResourceLinked(getActiveWslCodexHomePath(wslHome), firstLaunchHomePath)
|
||||
expect(readFileSync(join(firstLaunchHomePath, 'auth.json'), 'utf-8')).toBe(firstAuth)
|
||||
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(firstAuth)
|
||||
|
||||
@@ -1331,7 +1358,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
})
|
||||
service.syncForCurrentSelection(target)
|
||||
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(secondLaunchHomePath)
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(getActiveWslCodexHomePath(wslHome))
|
||||
expectResourceLinked(getActiveWslCodexHomePath(wslHome), secondLaunchHomePath)
|
||||
expect(readFileSync(join(secondLaunchHomePath, 'auth.json'), 'utf-8')).toBe(secondAuth)
|
||||
expect(readFileSync(join(firstLaunchHomePath, 'auth.json'), 'utf-8')).toBe(firstAuth)
|
||||
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(secondAuth)
|
||||
@@ -1491,18 +1519,21 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
|
||||
|
||||
const firstLaunchHomePath = service.prepareForCodexLaunch(target)
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(getActiveWslCodexHomePath(wslHome))
|
||||
const firstLaunchHomePath = getWslLaunchCodexHomePath(wslHome, 'account-1')
|
||||
settings.activeCodexManagedAccountIdsByRuntime = { host: null, wsl: { Ubuntu: 'account-2' } }
|
||||
const secondLaunchHomePath = service.prepareForCodexLaunch(target)
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(getActiveWslCodexHomePath(wslHome))
|
||||
const secondLaunchHomePath = getWslLaunchCodexHomePath(wslHome, 'account-2')
|
||||
|
||||
expect(readFileSync(join(firstLaunchHomePath!, 'auth.json'), 'utf-8')).toBe(firstAuth)
|
||||
expect(readFileSync(join(secondLaunchHomePath!, 'auth.json'), 'utf-8')).toBe(secondAuth)
|
||||
expect(lstatSync(join(firstLaunchHomePath!, 'auth.json')).isSymbolicLink()).toBe(false)
|
||||
expect(lstatSync(join(secondLaunchHomePath!, 'auth.json')).isSymbolicLink()).toBe(false)
|
||||
expectResourceLinked(getActiveWslCodexHomePath(wslHome), secondLaunchHomePath)
|
||||
expect(readFileSync(join(firstLaunchHomePath, 'auth.json'), 'utf-8')).toBe(firstAuth)
|
||||
expect(readFileSync(join(secondLaunchHomePath, 'auth.json'), 'utf-8')).toBe(secondAuth)
|
||||
expect(lstatSync(join(firstLaunchHomePath, 'auth.json')).isSymbolicLink()).toBe(false)
|
||||
expect(lstatSync(join(secondLaunchHomePath, 'auth.json')).isSymbolicLink()).toBe(false)
|
||||
for (const entryName of sqliteEntries) {
|
||||
const sharedPath = join(wslRuntimeHomePath, entryName)
|
||||
expectResourceLinked(join(firstLaunchHomePath!, entryName), sharedPath)
|
||||
expectResourceLinked(join(secondLaunchHomePath!, entryName), sharedPath)
|
||||
expectResourceLinked(join(firstLaunchHomePath, entryName), sharedPath)
|
||||
expectResourceLinked(join(secondLaunchHomePath, entryName), sharedPath)
|
||||
}
|
||||
} finally {
|
||||
if (originalPlatform) {
|
||||
@@ -1585,8 +1616,9 @@ describe('CodexRuntimeHomeService', () => {
|
||||
|
||||
expect(readFileSync(getRuntimeCodexAuthPath(), 'utf-8')).toBe(hostAuth)
|
||||
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
|
||||
wslLaunchHomePath
|
||||
getActiveWslCodexHomePath(wslHome)
|
||||
)
|
||||
expectResourceLinked(getActiveWslCodexHomePath(wslHome), wslLaunchHomePath)
|
||||
expect(readFileSync(join(wslManagedHomePath, 'auth.json'), 'utf-8')).toBe(wslManagedAuth)
|
||||
expect(readFileSync(join(wslLaunchHomePath, 'auth.json'), 'utf-8')).toBe(wslManagedAuth)
|
||||
} finally {
|
||||
@@ -1641,7 +1673,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(wslLaunchHomePath)
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(getActiveWslCodexHomePath(wslHome))
|
||||
expectResourceLinked(getActiveWslCodexHomePath(wslHome), wslLaunchHomePath)
|
||||
writeFileSync(runtimeAuthPath, staleRuntimeAuth, 'utf-8')
|
||||
writeFileSync(managedAuthPath, reauthedAuth, 'utf-8')
|
||||
|
||||
@@ -1706,7 +1739,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
|
||||
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(wslLaunchHomePath)
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(getActiveWslCodexHomePath(wslHome))
|
||||
expectResourceLinked(getActiveWslCodexHomePath(wslHome), wslLaunchHomePath)
|
||||
expect(readFileSync(join(managedHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
|
||||
expect(readFileSync(join(wslLaunchHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
|
||||
expect(readFileSync(join(getWslRuntimeCodexHomePath(wslHome), 'auth.json'), 'utf-8')).toBe(
|
||||
@@ -1865,14 +1899,16 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const systemLaunchHomePath = getWslLaunchCodexHomePath(wslHome, null)
|
||||
const managedLaunchHomePath = getWslLaunchCodexHomePath(wslHome, 'account-1')
|
||||
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(systemLaunchHomePath)
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(getActiveWslCodexHomePath(wslHome))
|
||||
expectResourceLinked(getActiveWslCodexHomePath(wslHome), systemLaunchHomePath)
|
||||
writeFileSync(join(systemLaunchHomePath, 'auth.json'), refreshedSystemAuth, 'utf-8')
|
||||
|
||||
store.updateSettings({
|
||||
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: 'account-1' } }
|
||||
})
|
||||
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(managedLaunchHomePath)
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(getActiveWslCodexHomePath(wslHome))
|
||||
expectResourceLinked(getActiveWslCodexHomePath(wslHome), managedLaunchHomePath)
|
||||
expect(readFileSync(join(systemCodexHomePath, 'auth.json'), 'utf-8')).toBe(
|
||||
refreshedSystemAuth
|
||||
)
|
||||
@@ -1975,10 +2011,12 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
|
||||
const wslLaunchHomePath = getWslLaunchCodexHomePath(wslHome, null)
|
||||
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(wslLaunchHomePath)
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(getActiveWslCodexHomePath(wslHome))
|
||||
expectResourceLinked(getActiveWslCodexHomePath(wslHome), wslLaunchHomePath)
|
||||
writeFileSync(join(wslLaunchHomePath, 'auth.json'), refreshedAuth, 'utf-8')
|
||||
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(wslLaunchHomePath)
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(getActiveWslCodexHomePath(wslHome))
|
||||
expectResourceLinked(getActiveWslCodexHomePath(wslHome), wslLaunchHomePath)
|
||||
expect(readFileSync(join(systemCodexHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
|
||||
expect(readFileSync(join(wslLaunchHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
|
||||
} finally {
|
||||
@@ -2022,7 +2060,8 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
|
||||
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(wslLaunchHomePath)
|
||||
expect(service.prepareForCodexLaunch(target)).toBe(getActiveWslCodexHomePath(wslHome))
|
||||
expectResourceLinked(getActiveWslCodexHomePath(wslHome), wslLaunchHomePath)
|
||||
expect(readFileSync(join(systemCodexHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
|
||||
expect(readFileSync(join(wslLaunchHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
|
||||
} finally {
|
||||
|
||||
@@ -26,8 +26,10 @@ import {
|
||||
import {
|
||||
ensureOrcaCodexLaunchHome,
|
||||
ensureScopedCodexLaunchHome,
|
||||
materializeOrcaCodexActiveHome,
|
||||
materializeOrcaCodexLaunchHome,
|
||||
materializeScopedCodexLaunchHome,
|
||||
pointActiveCodexHomeAtLaunchHome,
|
||||
removeOrcaCodexLaunchHome,
|
||||
removeScopedCodexLaunchHome
|
||||
} from '../codex/codex-launch-home-paths'
|
||||
@@ -97,6 +99,7 @@ export class CodexRuntimeHomeService {
|
||||
this.safeMigrateLegacyManagedState()
|
||||
this.initializeLastSyncedState()
|
||||
this.safeSyncForCurrentSelection()
|
||||
this.safeRefreshCurrentHostActiveHome()
|
||||
}
|
||||
|
||||
private initializeLastSyncedState(): void {
|
||||
@@ -116,16 +119,16 @@ export class CodexRuntimeHomeService {
|
||||
prepareForCodexLaunch(target?: CodexAccountSelectionTarget): string | null {
|
||||
if (target?.runtime === 'wsl') {
|
||||
const wslTarget = this.resolveWslDefaultTarget(target)
|
||||
return (
|
||||
const launchHomePath =
|
||||
this.syncWslRuntimeForCurrentSelection(wslTarget) ??
|
||||
this.getWslSystemCodexHomePath(wslTarget)
|
||||
)
|
||||
return this.pointWslActiveHomeForLaunch(wslTarget, launchHomePath)
|
||||
}
|
||||
this.syncForCurrentSelection()
|
||||
syncSystemCodexResourcesIntoManagedHome()
|
||||
syncSystemConfigIntoManagedCodexHome()
|
||||
syncSystemCodexSessionsIntoManagedHome()
|
||||
return this.materializeCurrentHostLaunchHome()
|
||||
return this.materializeCurrentHostActiveHome()
|
||||
}
|
||||
|
||||
private getWslSystemCodexHomePath(target: CodexAccountSelectionTarget): string | null {
|
||||
@@ -154,15 +157,29 @@ export class CodexRuntimeHomeService {
|
||||
return this.materializeCurrentHostLaunchHome()
|
||||
}
|
||||
|
||||
refreshCurrentHostLaunchHome(): string | null {
|
||||
refreshCurrentHostActiveHome(): string | null {
|
||||
try {
|
||||
return this.materializeCurrentHostLaunchHome()
|
||||
syncSystemCodexResourcesIntoManagedHome()
|
||||
syncSystemConfigIntoManagedCodexHome()
|
||||
syncSystemCodexSessionsIntoManagedHome()
|
||||
return this.materializeCurrentHostActiveHome()
|
||||
} catch (error) {
|
||||
console.warn('[codex-runtime-home] Failed to refresh host launch home:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
refreshCurrentLaunchHome(target?: CodexAccountSelectionTarget): string | null {
|
||||
if (target?.runtime === 'wsl') {
|
||||
const wslTarget = this.resolveWslDefaultTarget(target)
|
||||
const launchHomePath =
|
||||
this.syncWslRuntimeForCurrentSelection(wslTarget) ??
|
||||
this.getWslSystemCodexHomePath(wslTarget)
|
||||
return this.pointWslActiveHomeForLaunch(wslTarget, launchHomePath)
|
||||
}
|
||||
return this.refreshCurrentHostActiveHome()
|
||||
}
|
||||
|
||||
syncForCurrentSelection(target?: CodexAccountSelectionTarget): void {
|
||||
if (target?.runtime === 'wsl') {
|
||||
this.syncWslRuntimeForCurrentSelection(target)
|
||||
@@ -456,6 +473,14 @@ export class CodexRuntimeHomeService {
|
||||
}
|
||||
}
|
||||
|
||||
private safeRefreshCurrentHostActiveHome(): void {
|
||||
try {
|
||||
this.refreshCurrentHostActiveHome()
|
||||
} catch (error) {
|
||||
console.warn('[codex-runtime-home] Failed to refresh active host Codex home:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private getActiveAccount(
|
||||
accounts: CodexManagedAccount[],
|
||||
activeAccountId: string | null
|
||||
@@ -1065,6 +1090,35 @@ export class CodexRuntimeHomeService {
|
||||
return launchHomePath
|
||||
}
|
||||
|
||||
private materializeCurrentHostActiveHome(): string {
|
||||
const launchHomePath = this.materializeCurrentHostLaunchHome()
|
||||
// Why: terminals keep CODEX_HOME for their lifetime. Pointing that stable
|
||||
// path at the selected launch home restores hot-swap for the next `codex`.
|
||||
return materializeOrcaCodexActiveHome(launchHomePath)
|
||||
}
|
||||
|
||||
private pointWslActiveHomeAtLaunchHome(runtimeHomePath: string, launchHomePath: string): string {
|
||||
const activeHomePath = this.joinWslPath(dirname(runtimeHomePath), 'active', 'wsl', 'home')
|
||||
return pointActiveCodexHomeAtLaunchHome(activeHomePath, launchHomePath)
|
||||
}
|
||||
|
||||
private pointWslActiveHomeForLaunch(
|
||||
target: CodexAccountSelectionTarget,
|
||||
launchHomePath: string | null
|
||||
): string | null {
|
||||
if (!launchHomePath || process.platform !== 'win32') {
|
||||
return launchHomePath
|
||||
}
|
||||
const distro = target.wslDistro?.trim() || getDefaultWslDistro()
|
||||
if (!distro) {
|
||||
return launchHomePath
|
||||
}
|
||||
const runtimeHomePath = this.getWslRuntimeHomePath(distro)
|
||||
return runtimeHomePath
|
||||
? this.pointWslActiveHomeAtLaunchHome(runtimeHomePath, launchHomePath)
|
||||
: launchHomePath
|
||||
}
|
||||
|
||||
private getHostLaunchSelectionKey(accountId: string | null): string {
|
||||
return accountId ?? 'system'
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ function createRateLimits() {
|
||||
function createRuntimeHome() {
|
||||
return {
|
||||
syncForCurrentSelection: vi.fn(),
|
||||
refreshCurrentLaunchHome: vi.fn(() => null),
|
||||
clearLastWrittenAuthJson: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +156,7 @@ export class CodexAccountService {
|
||||
this.safeSyncCanonicalConfigToManagedHomes()
|
||||
this.runtimeHome.clearLastWrittenAuthJson(account.id)
|
||||
this.runtimeHome.syncForCurrentSelection()
|
||||
this.runtimeHome.refreshCurrentLaunchHome(targetSelection)
|
||||
|
||||
// Why: the new account becomes active, so the previous active account is
|
||||
// now inactive and its last-known usage should be cached for the switcher.
|
||||
@@ -200,6 +201,7 @@ export class CodexAccountService {
|
||||
this.safeSyncCanonicalConfigToManagedHomes()
|
||||
this.runtimeHome.clearLastWrittenAuthJson(accountId)
|
||||
this.runtimeHome.syncForCurrentSelection(getCodexSelectionTargetForAccount(account))
|
||||
this.runtimeHome.refreshCurrentLaunchHome(getCodexSelectionTargetForAccount(account))
|
||||
|
||||
// Why: re-auth can change which actual Codex identity the managed home
|
||||
// points at. Force a fresh read immediately so the status bar cannot keep
|
||||
@@ -228,6 +230,7 @@ export class CodexAccountService {
|
||||
activeCodexManagedAccountIdsByRuntime: nextSelection
|
||||
})
|
||||
this.runtimeHome.syncForCurrentSelection()
|
||||
this.runtimeHome.refreshCurrentLaunchHome(getCodexSelectionTargetForAccount(account))
|
||||
|
||||
this.runtimeHome.removeLaunchHomeForAccount?.(account)
|
||||
this.safeRemoveManagedHome(account.managedHomePath)
|
||||
@@ -276,6 +279,7 @@ export class CodexAccountService {
|
||||
})
|
||||
this.safeSyncCanonicalConfigToManagedHomes()
|
||||
this.runtimeHome.syncForCurrentSelection(effectiveTarget)
|
||||
this.runtimeHome.refreshCurrentLaunchHome(effectiveTarget)
|
||||
|
||||
await this.rateLimits.refreshForCodexAccountChange(outgoingAccountId, effectiveTarget)
|
||||
return this.getSnapshot()
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
readFileSync,
|
||||
readlinkSync,
|
||||
readdirSync,
|
||||
renameSync,
|
||||
rmdirSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
@@ -77,6 +78,70 @@ export function removeOrcaCodexLaunchHome(accountId: string): void {
|
||||
)
|
||||
}
|
||||
|
||||
export function materializeOrcaCodexActiveHome(launchHomePath: string): string {
|
||||
return pointActiveCodexHomeAtLaunchHome(getOrcaCodexActiveHostHomePath(), launchHomePath)
|
||||
}
|
||||
|
||||
export function pointActiveCodexHomeAtLaunchHome(
|
||||
activeHomePath: string,
|
||||
launchHomePath: string
|
||||
): string {
|
||||
if (pointWslActiveCodexHomeAtLaunchHome(activeHomePath, launchHomePath)) {
|
||||
return activeHomePath
|
||||
}
|
||||
mkdirSync(dirname(activeHomePath), { recursive: true })
|
||||
if (activeHomeAlreadyPointsToLaunchHome(activeHomePath, launchHomePath)) {
|
||||
return activeHomePath
|
||||
}
|
||||
|
||||
const nextLinkPath = `${activeHomePath}.next-${process.pid}-${Date.now()}`
|
||||
removeActiveHomeLinkIfOwned(nextLinkPath)
|
||||
try {
|
||||
createSharedEntryLink(launchHomePath, nextLinkPath)
|
||||
replaceActiveHomeLink(activeHomePath, nextLinkPath)
|
||||
return activeHomePath
|
||||
} catch (error) {
|
||||
removeActiveHomeLinkIfOwned(nextLinkPath)
|
||||
console.warn('[codex-home] Failed to point active Codex home at launch home:', error)
|
||||
return launchHomePath
|
||||
}
|
||||
}
|
||||
|
||||
function pointWslActiveCodexHomeAtLaunchHome(
|
||||
activeHomePath: string,
|
||||
launchHomePath: string
|
||||
): boolean {
|
||||
const paths = getSameDistroWslPaths(launchHomePath, activeHomePath)
|
||||
if (!paths) {
|
||||
return false
|
||||
}
|
||||
const nextLinuxPath = `${paths.targetLinuxPath}.next-${process.pid}-${Date.now()}`
|
||||
try {
|
||||
execFileSync(
|
||||
'wsl.exe',
|
||||
[
|
||||
'-d',
|
||||
paths.distro,
|
||||
'--',
|
||||
'bash',
|
||||
'-lc',
|
||||
'mkdir -p "$(dirname "$2")" && rm -f -- "$3" && ln -s -- "$1" "$3" && mv -Tf -- "$3" "$2"',
|
||||
'sh',
|
||||
paths.sourceLinuxPath,
|
||||
paths.targetLinuxPath,
|
||||
nextLinuxPath
|
||||
],
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 5000
|
||||
}
|
||||
)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function getScopedCodexLaunchHomePath(
|
||||
launchRootPath: string,
|
||||
accountId: string | null
|
||||
@@ -151,6 +216,10 @@ function getOrcaCodexLaunchHostRootPathWithOptions(options: { create: boolean })
|
||||
return rootPath
|
||||
}
|
||||
|
||||
function getOrcaCodexActiveHostHomePath(): string {
|
||||
return join(dirname(getOrcaManagedCodexHomePath()), 'active', 'host', 'home')
|
||||
}
|
||||
|
||||
function resolveCodexLaunchHomePath(launchRootPath: string, accountId: string | null): string {
|
||||
return join(launchRootPath, getLaunchSelectionSegment(accountId), 'home')
|
||||
}
|
||||
@@ -403,6 +472,62 @@ function removeWslPathIfPossible(targetPath: string): boolean {
|
||||
return runWslPathCommand(targetWsl.distro, ['rm', '-rf', '--', targetWsl.linuxPath])
|
||||
}
|
||||
|
||||
function activeHomeAlreadyPointsToLaunchHome(
|
||||
activeHomePath: string,
|
||||
launchHomePath: string
|
||||
): boolean {
|
||||
if (targetAlreadyPointsToSource(activeHomePath, launchHomePath)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function replaceActiveHomeLink(activeHomePath: string, nextLinkPath: string): void {
|
||||
try {
|
||||
renameSync(nextLinkPath, activeHomePath)
|
||||
} catch (error) {
|
||||
if (!activeHomeLinkIsReplaceable(activeHomePath)) {
|
||||
throw error
|
||||
}
|
||||
removeActiveHomeLinkIfOwned(activeHomePath)
|
||||
renameSync(nextLinkPath, activeHomePath)
|
||||
}
|
||||
}
|
||||
|
||||
function activeHomeLinkIsReplaceable(activeHomePath: string): boolean {
|
||||
try {
|
||||
const stat = lstatSync(activeHomePath)
|
||||
return stat.isSymbolicLink() || isWindowsReadableLink(activeHomePath)
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function removeActiveHomeLinkIfOwned(activeHomePath: string): void {
|
||||
try {
|
||||
const stat = lstatSync(activeHomePath)
|
||||
if (stat.isSymbolicLink()) {
|
||||
unlinkSync(activeHomePath)
|
||||
} else if (isWindowsReadableLink(activeHomePath)) {
|
||||
rmdirSync(activeHomePath)
|
||||
}
|
||||
} catch {
|
||||
// Missing or inaccessible temporary links are handled by the caller.
|
||||
}
|
||||
}
|
||||
|
||||
function isWindowsReadableLink(targetPath: string): boolean {
|
||||
if (process.platform !== 'win32') {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
readlinkSync(targetPath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function readWslSymlinkTarget(targetPath: string): string | null {
|
||||
if (process.platform !== 'win32') {
|
||||
return null
|
||||
@@ -708,6 +833,15 @@ function targetAlreadyPointsToSource(targetPath: string, sourcePath: string): bo
|
||||
if (targetStat.isSymbolicLink()) {
|
||||
return linkTargetsMatch(readlinkSync(targetPath), sourcePath)
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
if (linkTargetsMatch(readlinkSync(targetPath), sourcePath)) {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
// Non-link files fall through to the hard-link identity check below.
|
||||
}
|
||||
}
|
||||
if (!targetStat.isFile() || !existsSync(sourcePath)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ vi.mock('../providers/local-pty-utils', async (importOriginal) => {
|
||||
import { createPtySubprocess } from './pty-subprocess'
|
||||
|
||||
const ORCA_SHELL_WRAPPER_ENV = [
|
||||
'CODEX_HOME',
|
||||
'ORCA_ATTRIBUTION_SHIM_DIR',
|
||||
'ORCA_OPENCODE_CONFIG_DIR',
|
||||
'ORCA_PI_CODING_AGENT_DIR',
|
||||
|
||||
+1
-1
@@ -404,7 +404,7 @@ function prepareCodexRuntimeHomeForLaunch(target?: CodexAccountSelectionTarget):
|
||||
)
|
||||
}
|
||||
if (target?.runtime !== 'wsl') {
|
||||
return codexRuntimeHome!.refreshCurrentHostLaunchHome() ?? runtimeHomePath
|
||||
return codexRuntimeHome!.refreshCurrentHostActiveHome() ?? runtimeHomePath
|
||||
}
|
||||
return runtimeHomePath
|
||||
}
|
||||
|
||||
@@ -2710,7 +2710,7 @@ describe('registerPtyHandlers', () => {
|
||||
expect(spawnOptions.env.ORCA_CODEX_HOME).toBeUndefined()
|
||||
})
|
||||
|
||||
it('converts selected WSL Codex homes to Linux paths for wsl.exe shells', () => {
|
||||
it('converts selected WSL active Codex homes to Linux paths for wsl.exe shells', () => {
|
||||
const originalPlatform = process.platform
|
||||
Object.defineProperty(process, 'platform', {
|
||||
configurable: true,
|
||||
@@ -2724,7 +2724,7 @@ describe('registerPtyHandlers', () => {
|
||||
mainWindow as never,
|
||||
undefined,
|
||||
() =>
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\test\\.local\\share\\orca\\codex-runtime-home\\launch\\wsl\\system\\home',
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\test\\.local\\share\\orca\\codex-runtime-home\\active\\wsl\\home',
|
||||
() =>
|
||||
({
|
||||
terminalWindowsShell: 'powershell.exe',
|
||||
@@ -2739,10 +2739,10 @@ describe('registerPtyHandlers', () => {
|
||||
|
||||
const spawnOptions = spawnMock.mock.calls.at(-1)?.[2] as { env: Record<string, string> }
|
||||
expect(spawnOptions.env.CODEX_HOME).toBe(
|
||||
'/home/test/.local/share/orca/codex-runtime-home/launch/wsl/system/home'
|
||||
'/home/test/.local/share/orca/codex-runtime-home/active/wsl/home'
|
||||
)
|
||||
expect(spawnOptions.env.ORCA_CODEX_HOME).toBe(
|
||||
'/home/test/.local/share/orca/codex-runtime-home/launch/wsl/system/home'
|
||||
'/home/test/.local/share/orca/codex-runtime-home/active/wsl/home'
|
||||
)
|
||||
expect(spawnOptions.env.WSLENV).toContain('CODEX_HOME')
|
||||
expect(spawnOptions.env.WSLENV).toContain('ORCA_CODEX_HOME')
|
||||
|
||||
@@ -69,13 +69,19 @@ describe('LocalPtyProvider', () => {
|
||||
}
|
||||
let exitCb: ((info: { exitCode: number }) => void) | undefined
|
||||
let origShell: string | undefined
|
||||
let origCodexHome: string | undefined
|
||||
let origOrcaCodexHome: string | undefined
|
||||
let origPlatform: PropertyDescriptor | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
origPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' })
|
||||
origShell = process.env.SHELL
|
||||
origCodexHome = process.env.CODEX_HOME
|
||||
origOrcaCodexHome = process.env.ORCA_CODEX_HOME
|
||||
process.env.SHELL = '/bin/zsh'
|
||||
delete process.env.CODEX_HOME
|
||||
delete process.env.ORCA_CODEX_HOME
|
||||
|
||||
existsSyncMock.mockReturnValue(true)
|
||||
statSyncMock.mockReturnValue({ isDirectory: () => true, mode: 0o755 })
|
||||
@@ -111,6 +117,16 @@ describe('LocalPtyProvider', () => {
|
||||
} else {
|
||||
process.env.SHELL = origShell
|
||||
}
|
||||
if (origCodexHome === undefined) {
|
||||
delete process.env.CODEX_HOME
|
||||
} else {
|
||||
process.env.CODEX_HOME = origCodexHome
|
||||
}
|
||||
if (origOrcaCodexHome === undefined) {
|
||||
delete process.env.ORCA_CODEX_HOME
|
||||
} else {
|
||||
process.env.ORCA_CODEX_HOME = origOrcaCodexHome
|
||||
}
|
||||
})
|
||||
|
||||
describe('spawn', () => {
|
||||
|
||||
Reference in New Issue
Block a user