mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 16:03:26 +00:00
@@ -140,6 +140,10 @@ Done means: Local, SSH, remote server, and future cloud VMs share one Host model
|
||||
- [~] Integration-test Linear/Jira with explicit source account selection.
|
||||
- [x] Integration-test automations on local host, SSH host, and remote server where supported.
|
||||
- [x] Integration-test disconnected host UX for Tasks, New Workspace modal, sidebar, and automations.
|
||||
- 2026-06-13 follow-up: verified patched main IPC against a live Docker SSH target.
|
||||
Connected SSH PTY inspection returned `bash`; after disconnect, PTY inspection returned
|
||||
`null`/`false` and remote agent detection returned `[]` without repeated main-process
|
||||
handler errors.
|
||||
- [ ] Integration-test new client with older remote server and older client with newer server where practical.
|
||||
- [x] Electron-test Add Project flows: browse folder, clone URL, and create project from scratch on local and SSH hosts.
|
||||
- [x] Electron-test workspace options and sidebar host filtering.
|
||||
|
||||
@@ -273,6 +273,12 @@
|
||||
Docker SSH target through Orca IPC, added an SSH project, created an SSH worktree, tested
|
||||
disconnected SSH UX, opened Tasks, opened Automations, and captured screenshots below.
|
||||
</li>
|
||||
<li class="pass">
|
||||
Follow-up disconnected SSH validation launched a patched second Electron instance,
|
||||
connected a Docker SSH PTY, observed foreground process <code>bash</code>, disconnected
|
||||
the host, then verified PTY inspection returned <code>null</code>/<code>false</code>
|
||||
and remote agent detection returned <code>[]</code> without repeated handler errors.
|
||||
</li>
|
||||
<li class="pass">
|
||||
Focused locked-SSH Add Project regression test passed:
|
||||
<code>src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx</code>,
|
||||
|
||||
@@ -589,6 +589,31 @@ describe('preflight', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('returns no remote agents when the SSH connection is unavailable', async () => {
|
||||
getActiveMultiplexerMock.mockReturnValue(null)
|
||||
|
||||
registerPreflightHandlers()
|
||||
|
||||
await expect(
|
||||
handlers['preflight:detectRemoteAgents'](undefined, { connectionId: 'ssh-1' })
|
||||
).resolves.toEqual([])
|
||||
})
|
||||
|
||||
it('returns no remote agents when the SSH connection is disposed', async () => {
|
||||
const request = vi.fn()
|
||||
getActiveMultiplexerMock.mockReturnValue({
|
||||
isDisposed: () => true,
|
||||
request
|
||||
})
|
||||
|
||||
registerPreflightHandlers()
|
||||
|
||||
await expect(
|
||||
handlers['preflight:detectRemoteAgents'](undefined, { connectionId: 'ssh-1' })
|
||||
).resolves.toEqual([])
|
||||
expect(request).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('detects agents from the selected WSL distro for a WSL workspace', async () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
configurable: true,
|
||||
|
||||
@@ -248,7 +248,9 @@ export async function refreshShellPathAndDetectAgents(
|
||||
export async function detectRemoteAgents(args: { connectionId: string }): Promise<string[]> {
|
||||
const mux = getActiveMultiplexer(args.connectionId)
|
||||
if (!mux || mux.isDisposed()) {
|
||||
throw new Error(`No active SSH connection for "${args.connectionId}"`)
|
||||
// Why: remote agent detection is passive UI polling. A disconnected host has
|
||||
// no detectable agents until reconnect, but should not spam IPC errors.
|
||||
return []
|
||||
}
|
||||
const result = (await mux.request('preflight.detectAgents', {
|
||||
commands: KNOWN_AGENT_COMMANDS
|
||||
|
||||
@@ -2831,6 +2831,43 @@ describe('registerPtyHandlers', () => {
|
||||
expect(store.markSshRemotePtyLease).toHaveBeenCalledWith('ssh-1', 'remote-pty', 'terminated')
|
||||
})
|
||||
|
||||
it('returns idle process inspection results for detached SSH PTYs without a provider', async () => {
|
||||
const provider = {
|
||||
spawn: vi.fn(),
|
||||
write: vi.fn(),
|
||||
resize: vi.fn(),
|
||||
shutdown: vi.fn(),
|
||||
sendSignal: vi.fn(),
|
||||
getCwd: vi.fn(),
|
||||
getInitialCwd: vi.fn(),
|
||||
clearBuffer: vi.fn(),
|
||||
acknowledgeDataEvent: vi.fn(),
|
||||
onData: vi.fn(() => () => {}),
|
||||
onReplay: vi.fn(() => () => {}),
|
||||
onExit: vi.fn(() => () => {}),
|
||||
listProcesses: vi.fn(),
|
||||
hasChildProcesses: vi.fn(),
|
||||
getForegroundProcess: vi.fn(),
|
||||
serialize: vi.fn(),
|
||||
revive: vi.fn(),
|
||||
getDefaultShell: vi.fn(),
|
||||
getProfiles: vi.fn()
|
||||
}
|
||||
registerSshPtyProvider('ssh-1', provider as never)
|
||||
registerPtyHandlers(mainWindow as never)
|
||||
setPtyOwnership('remote-pty', 'ssh-1')
|
||||
unregisterSshPtyProvider('ssh-1')
|
||||
|
||||
await expect(handlers.get('pty:hasChildProcesses')!(null, { id: 'remote-pty' })).resolves.toBe(
|
||||
false
|
||||
)
|
||||
await expect(
|
||||
handlers.get('pty:getForegroundProcess')!(null, { id: 'remote-pty' })
|
||||
).resolves.toBeNull()
|
||||
expect(provider.hasChildProcesses).not.toHaveBeenCalled()
|
||||
expect(provider.getForegroundProcess).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('injects ORCA_TERMINAL_HANDLE for non-local PTY providers', async () => {
|
||||
const spawn = vi.fn(async () => ({ id: 'remote-pty' }))
|
||||
registerSshPtyProvider('ssh-1', {
|
||||
|
||||
@@ -226,6 +226,13 @@ function getProviderForPty(ptyId: string): IPtyProvider {
|
||||
return getProvider(connectionId)
|
||||
}
|
||||
|
||||
function hasPtyProviderForInspection(ptyId: string): boolean {
|
||||
// Why: process inspection is background polling; disconnected SSH hosts should
|
||||
// read as idle instead of surfacing repeated IPC errors.
|
||||
const connectionId = ptyOwnership.get(ptyId)
|
||||
return connectionId == null || sshProviders.has(connectionId)
|
||||
}
|
||||
|
||||
function getAppPtyId(connectionId: string | null | undefined, ptyId: string): string {
|
||||
return connectionId ? toAppSshPtyId(connectionId, ptyId) : ptyId
|
||||
}
|
||||
@@ -2801,6 +2808,9 @@ export function registerPtyHandlers(
|
||||
ipcMain.handle(
|
||||
'pty:hasChildProcesses',
|
||||
async (_event, args: { id: string }): Promise<boolean> => {
|
||||
if (!hasPtyProviderForInspection(args.id)) {
|
||||
return false
|
||||
}
|
||||
return getProviderForPty(args.id).hasChildProcesses(args.id)
|
||||
}
|
||||
)
|
||||
@@ -2808,6 +2818,9 @@ export function registerPtyHandlers(
|
||||
ipcMain.handle(
|
||||
'pty:getForegroundProcess',
|
||||
async (_event, args: { id: string }): Promise<string | null> => {
|
||||
if (!hasPtyProviderForInspection(args.id)) {
|
||||
return null
|
||||
}
|
||||
return getProviderForPty(args.id).getForegroundProcess(args.id)
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user