diff --git a/config/scripts/node-server-verifier-daemon-cleanup.test.ts b/config/scripts/node-server-verifier-daemon-cleanup.test.ts index 5809c0862b6..5f566e29aab 100644 --- a/config/scripts/node-server-verifier-daemon-cleanup.test.ts +++ b/config/scripts/node-server-verifier-daemon-cleanup.test.ts @@ -38,8 +38,8 @@ describe('stopNodeServerVerifierDaemons', () => { temporaryPaths.push(dataPath) const daemonPath = join(dataPath, 'daemon') mkdirSync(daemonPath) - writeFileSync(join(daemonPath, 'daemon-v4.pid'), JSON.stringify({ pid: 4101 })) writeFileSync(join(daemonPath, 'daemon-v5.pid'), JSON.stringify({ pid: 5101 })) + writeFileSync(join(daemonPath, 'daemon-v4.pid'), JSON.stringify({ pid: 4101 })) vi.spyOn(process, 'kill').mockImplementation(() => true) const outcome = stopNodeServerVerifierDaemons(dataPath).catch((error: unknown) => error) diff --git a/config/scripts/node-server-verifier-daemon-cleanup.ts b/config/scripts/node-server-verifier-daemon-cleanup.ts index 291836728ad..d541bc34a35 100644 --- a/config/scripts/node-server-verifier-daemon-cleanup.ts +++ b/config/scripts/node-server-verifier-daemon-cleanup.ts @@ -9,7 +9,7 @@ export async function stopNodeServerVerifierDaemons(dataPath: string): Promise[] = [] - for (const entry of readdirSync(daemonPath)) { + for (const entry of readdirSync(daemonPath).sort()) { const match = /^daemon-v(\d+)\.pid$/.exec(entry) if (!match) { continue diff --git a/src/main/ipc/worktree-push-target-setup.test.ts b/src/main/ipc/worktree-push-target-setup.test.ts index ce9df201633..6f08f29648a 100644 --- a/src/main/ipc/worktree-push-target-setup.test.ts +++ b/src/main/ipc/worktree-push-target-setup.test.ts @@ -156,6 +156,7 @@ describe('prepareWorktreePushTargetWithExec', () => { remoteUrl: FORK_SSH, remoteCreated: true }) + expect(onRemoteCreated).toHaveBeenCalledTimes(1) }) it('inherits remoteCreated when the predicate says a known worktree created the reused remote', async () => { diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index 21ceac9036b..bea7cdf6717 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -1050,7 +1050,6 @@ export async function prepareWorktreePushTarget( repoId?: string, gitOptions: { wslDistro?: string; signal?: AbortSignal } = {}, options: { - createdWorktreeId?: string onRemoteCreated?: (createdTarget: GitPushTarget) => void onRemoteLifecycleAcquired?: (release: () => void) => void } = {} @@ -1059,7 +1058,6 @@ export async function prepareWorktreePushTarget( localPushTargetRemoteLifecycleScope(repoPath, repoId, gitOptions.wslDistro) ) let lifecycleTransferred = false - let createdRemote: GitPushTarget | null = null try { if (options.onRemoteLifecycleAcquired) { options.onRemoteLifecycleAcquired(releaseLifecycle) @@ -1079,34 +1077,9 @@ export async function prepareWorktreePushTarget( ) : false, { - onRemoteCreated: (createdTarget) => { - createdRemote = createdTarget - options.onRemoteCreated?.(createdTarget) - } + onRemoteCreated: options.onRemoteCreated } ) - } catch (error) { - if (createdRemote && store && options.createdWorktreeId) { - try { - const rollbackSignal = createWorktreeRollbackSignal() - await cleanupUnusedWorktreePushTargetRemoteWithExec( - repoPath, - options.createdWorktreeId, - createdRemote, - store, - (args, cwd) => - gitExecFileAsync(args, { - cwd, - ...gitOptions, - signal: rollbackSignal - }) - ) - createdRemote = null - } catch (cleanupError) { - throw new AggregateError([error, cleanupError], 'Fork remote preparation rollback failed.') - } - } - throw error } finally { if (!lifecycleTransferred) { releaseLifecycle() @@ -1188,7 +1161,6 @@ async function prepareWorktreePushTargetSsh( repoId?: string, options: { signal?: AbortSignal - createdWorktreeId?: string onRemoteCreated?: (createdTarget: GitPushTarget) => void onRemoteLifecycleAcquired?: (release: () => void) => void } = {} @@ -1201,7 +1173,6 @@ async function prepareWorktreePushTargetSsh( const { remoteCreated: _ignoredRemoteCreated, ...sanitizedTarget } = target let remoteName = target.remoteName let remoteCreated = false - let createdRemote: GitPushTarget | null = null try { if (options.onRemoteLifecycleAcquired) { options.onRemoteLifecycleAcquired(releaseLifecycle) @@ -1229,8 +1200,7 @@ async function prepareWorktreePushTargetSsh( } else { remoteName = await ensureUniqueRemoteName(execGit, repoPath, target.remoteName) remoteCreated = true - createdRemote = { ...sanitizedTarget, remoteName, remoteCreated: true } - options.onRemoteCreated?.(createdRemote) + options.onRemoteCreated?.({ ...sanitizedTarget, remoteName, remoteCreated: true }) await provider.exec(['remote', 'add', remoteName, target.remoteUrl], repoPath, { signal: options.signal }) @@ -1244,23 +1214,6 @@ async function prepareWorktreePushTargetSsh( { signal: options.signal } ) return { ...sanitizedTarget, remoteName, ...(remoteCreated ? { remoteCreated: true } : {}) } - } catch (error) { - if (createdRemote && store && options.createdWorktreeId) { - try { - const rollbackSignal = createWorktreeRollbackSignal() - await cleanupUnusedWorktreePushTargetRemoteWithExec( - repoPath, - options.createdWorktreeId, - createdRemote, - store, - (args, cwd) => provider.exec(args, cwd, { signal: rollbackSignal }) - ) - createdRemote = null - } catch (cleanupError) { - throw new AggregateError([error, cleanupError], 'Remote fork preparation rollback failed.') - } - } - throw error } finally { if (!lifecycleTransferred) { releaseLifecycle() @@ -2064,7 +2017,6 @@ async function createRemoteWorktreeOperation( repo.id, { signal, - createdWorktreeId: rollbackWorktreeId, onRemoteCreated: (createdTarget) => { createdPushTargetRemote = createdTarget }, @@ -2729,7 +2681,6 @@ async function createLocalWorktreeOperation( repo.id, localWorktreeGitOptions, { - createdWorktreeId, onRemoteCreated: (createdTarget) => { createdPushTargetRemote = createdTarget }, diff --git a/src/main/ipc/worktrees.test.ts b/src/main/ipc/worktrees.test.ts index 2b878d8d23f..26be93a5327 100644 --- a/src/main/ipc/worktrees.test.ts +++ b/src/main/ipc/worktrees.test.ts @@ -2126,6 +2126,11 @@ describe('registerWorktreeHandlers', () => { ['remote', 'remove', 'pr-contributor-orca'], expect.objectContaining({ cwd: '/workspace/repo', signal: cleanupSignal }) ) + expect( + gitExecFileAsyncMock.mock.calls.filter( + ([gitArgs]) => gitArgs[0] === 'remote' && gitArgs[1] === 'remove' + ) + ).toHaveLength(1) }) it('removes a local fork remote when add mutates then rejects', async () => { @@ -2180,6 +2185,11 @@ describe('registerWorktreeHandlers', () => { ['remote', 'remove', 'pr-contributor-orca'], expect.objectContaining({ cwd: '/workspace/repo', signal: cleanupSignal }) ) + expect( + gitExecFileAsyncMock.mock.calls.filter( + ([gitArgs]) => gitArgs[0] === 'remote' && gitArgs[1] === 'remove' + ) + ).toHaveLength(1) }) it('keeps the Orca-created marker when a new worktree reuses an Orca-created fork remote', async () => { diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 77f5553d52d..027caa6257e 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -4385,6 +4385,9 @@ describe('OrcaRuntimeService', () => { ['remote', 'remove', 'pr-contributor-orca'], expect.objectContaining({ cwd: TEST_REPO_PATH, signal: cleanupSignal }) ) + expect( + gitSpy.mock.calls.filter(([gitArgs]) => gitArgs[0] === 'remote' && gitArgs[1] === 'remove') + ).toHaveLength(1) } finally { gitSpy.mockRestore() } diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 2389e152749..d2efcd3488d 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -22480,7 +22480,6 @@ export class OrcaRuntimeService { repo.id, localWorktreeGitOptions, { - createdWorktreeId, onRemoteCreated: (createdTarget) => { createdPushTargetRemote = createdTarget }, diff --git a/src/node-server/node-host-electron-facade.ts b/src/node-server/node-host-electron-facade.ts index 61ec5808365..f554d69ff83 100644 --- a/src/node-server/node-host-electron-facade.ts +++ b/src/node-server/node-host-electron-facade.ts @@ -1,4 +1,5 @@ import { EventEmitter } from 'node:events' +import { setGlobalProxyFromEnv } from 'node:http' import { homedir, tmpdir } from 'node:os' import { dirname, join } from 'node:path' @@ -171,6 +172,32 @@ export const webContents = { getAllWebContents: () => [] } +class NodeHostProxySession { + async resolveProxy(): Promise<'DIRECT'> { + return 'DIRECT' + } + + async setProxy(config: { + mode?: 'system' | 'fixed_servers' + proxyRules?: string + proxyBypassRules?: string + }): Promise { + if (config.mode === 'fixed_servers' && config.proxyRules) { + setGlobalProxyFromEnv({ + HTTP_PROXY: config.proxyRules, + HTTPS_PROXY: config.proxyRules, + NO_PROXY: config.proxyBypassRules?.replaceAll(';', ',') ?? '' + }) + return + } + setGlobalProxyFromEnv(process.env) + } + + async closeAllConnections(): Promise {} +} + +const nodeHostDefaultSession = new NodeHostProxySession() + export const safeStorage = { isEncryptionAvailable: () => false, encryptString: () => { @@ -236,8 +263,8 @@ export const screen = { } export const session = { - defaultSession: null, - fromPartition: () => null + defaultSession: nodeHostDefaultSession, + fromPartition: () => nodeHostDefaultSession } export const protocol = { registerSchemesAsPrivileged: () => {} } diff --git a/src/node-server/node-host-proxy-session.test.ts b/src/node-server/node-host-proxy-session.test.ts new file mode 100644 index 00000000000..414a025595b --- /dev/null +++ b/src/node-server/node-host-proxy-session.test.ts @@ -0,0 +1,31 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const setGlobalProxyFromEnv = vi.hoisted(() => vi.fn()) + +vi.mock('node:http', () => ({ setGlobalProxyFromEnv })) + +import { session } from './node-host-electron-facade' + +describe('Node host proxy session', () => { + beforeEach(() => { + setGlobalProxyFromEnv.mockReset() + }) + + it('reports direct routing so environment proxy settings can be applied', async () => { + await expect(session.defaultSession.resolveProxy()).resolves.toBe('DIRECT') + }) + + it('applies fixed proxy settings to Node fetch and HTTP clients', async () => { + await session.defaultSession.setProxy({ + mode: 'fixed_servers', + proxyRules: 'http://proxy.example:8080', + proxyBypassRules: 'localhost;*.internal' + }) + + expect(setGlobalProxyFromEnv).toHaveBeenCalledWith({ + HTTP_PROXY: 'http://proxy.example:8080', + HTTPS_PROXY: 'http://proxy.example:8080', + NO_PROXY: 'localhost,*.internal' + }) + }) +}) diff --git a/src/renderer/src/components/sidebar/AddRemoteHostFields.test.tsx b/src/renderer/src/components/sidebar/AddRemoteHostFields.test.tsx index 7e20ff22b68..04c7f99293f 100644 --- a/src/renderer/src/components/sidebar/AddRemoteHostFields.test.tsx +++ b/src/renderer/src/components/sidebar/AddRemoteHostFields.test.tsx @@ -31,9 +31,8 @@ describe('RemoteServerFields', () => { /> ) - expect(markup.indexOf('npx @stablyai/orca@latest')).toBeLessThan( - markup.indexOf('add-server-name') - ) + expect(markup).toContain('npx @stablyai/orca@rc') + expect(markup.indexOf('npx @stablyai/orca@rc')).toBeLessThan(markup.indexOf('add-server-name')) expect(markup).toContain('Copy server command') })