mirror of
https://github.com/stablyai/orca.git
synced 2026-11-06 12:07:58 +00:00
* fix(wsl): main's background terminal starts never start a stopped WSL distro Every start main makes through the runtime controller is background work unless its caller names the user's act (CLI and window creates and splits, paired-client creates and tab opens, agent launches and sessions, conversation resumes). #35813's hold now also holds those fresh and restored starts while the distro is stopped or unverifiable. * fix(wsl): headless Run now starts as the user's act; update create expectations A headless automation's Run now names the user's act through launchAgentTerminal, so it may start a stopped WSL distro; a scheduled run stays background and is held. The paired-client background create expectation now includes wslUserAct. * fix(wsl): main's background start checks a folder workspace's path passively With #35899's folder stat gate, the runtime preflight passes passiveWslRestore = !wslUserAct, so a background start never stats a stopped distro's folder as a user act before the daemon hold.
587 lines
21 KiB
TypeScript
587 lines
21 KiB
TypeScript
import {
|
|
independentActionEffortPlan,
|
|
consumePreparedBackgroundCommand
|
|
} from './prepared-action-launch.test-fixture'
|
|
import { createRuntimeAgentBackgroundTerminal } from '../../src/renderer/src/lib/runtime-agent-background-create'
|
|
import { createCompatibleRuntimeStatusResponseIfNeeded } from '@/runtime/runtime-compatibility-test-fixture'
|
|
import { AGENT_SESSION_KEYBOARD_RUNTIME_CAPABILITY } from '../../src/shared/protocol-version'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
AGENT_BACKGROUND_SESSION_UUID_RE as UUID_RE,
|
|
createAgentBackgroundSessionTestState,
|
|
resetAgentBackgroundSessionTestHarness,
|
|
useRemoteAgentBackgroundRuntime
|
|
} from '@/lib/agent-background-session-test-state'
|
|
|
|
const mockSpawn = vi.fn()
|
|
const mockKill = vi.fn()
|
|
const mockWrite = vi.fn()
|
|
const mockRuntimeEnvironmentCall = vi.fn()
|
|
const mockRuntimeEnvironmentTransportCall = vi.fn()
|
|
const mockRuntimeEnvironmentSubscribe = vi.fn()
|
|
const mockCreateTab = vi.fn()
|
|
const mockSetTabCustomTitle = vi.fn()
|
|
const mockUpdateTabPtyId = vi.fn()
|
|
const mockCloseTab = vi.fn()
|
|
const mockSetTabLayout = vi.fn()
|
|
const mockRegisterAgentLaunchConfig = vi.fn()
|
|
const mockRegisterEagerPtyBuffer = vi.fn()
|
|
const mockSubscribeToPtyData = vi.fn()
|
|
const mockSubscribeToPtyExit = vi.fn()
|
|
const mockPasteDraftWhenAgentReady = vi.fn()
|
|
const mockDispatchEvent = vi.fn()
|
|
const mockGetAgentLaunchPlatformForRepo = vi.fn<() => NodeJS.Platform>()
|
|
const state = createAgentBackgroundSessionTestState({
|
|
createTab: mockCreateTab,
|
|
setTabCustomTitle: mockSetTabCustomTitle,
|
|
updateTabPtyId: mockUpdateTabPtyId,
|
|
closeTab: mockCloseTab,
|
|
setTabLayout: mockSetTabLayout,
|
|
registerAgentLaunchConfig: mockRegisterAgentLaunchConfig
|
|
})
|
|
|
|
vi.mock('@/store', () => ({
|
|
useAppStore: {
|
|
getState: () => state,
|
|
subscribe: vi.fn(() => () => {})
|
|
}
|
|
}))
|
|
|
|
// Why: the background mount's sleeping-agent scan reads store state these launches never seed.
|
|
vi.mock('@/lib/sleeping-agent-wake-requests', () => ({ requestSleepingAgentWakeOnMount: vi.fn() }))
|
|
|
|
vi.mock('@/lib/telemetry', () => ({
|
|
track: vi.fn(),
|
|
tuiAgentToAgentKind: (agent: string) => agent
|
|
}))
|
|
|
|
vi.mock('@/lib/agent-paste-draft', () => ({
|
|
pasteDraftWhenAgentReady: mockPasteDraftWhenAgentReady
|
|
}))
|
|
|
|
vi.mock('@/lib/execution-host-facts', async (importOriginal) => {
|
|
const hostFact = () => ({
|
|
kind: 'known' as const,
|
|
platform: mockGetAgentLaunchPlatformForRepo(),
|
|
hostKind: 'local' as const
|
|
})
|
|
return {
|
|
...(await importOriginal<Record<string, unknown>>()),
|
|
resolveRepoExecutionHostPlatform: hostFact,
|
|
resolveWorktreeExecutionHostPlatform: hostFact
|
|
}
|
|
})
|
|
|
|
vi.mock('@/components/terminal-pane/pty-dispatcher', () => ({
|
|
registerEagerPtyBuffer: mockRegisterEagerPtyBuffer,
|
|
subscribeToPtyExit: mockSubscribeToPtyExit
|
|
}))
|
|
|
|
vi.mock('@/components/terminal-pane/pty-data-sidecar-subscriptions', () => ({
|
|
subscribeToPtyData: mockSubscribeToPtyData
|
|
}))
|
|
|
|
describe('launchAgentBackgroundSession remote runtime and SSH startup delivery', () => {
|
|
beforeEach(() => {
|
|
resetAgentBackgroundSessionTestHarness({
|
|
state,
|
|
createTab: mockCreateTab,
|
|
closeTab: mockCloseTab,
|
|
getLaunchPlatform: mockGetAgentLaunchPlatformForRepo,
|
|
runtimeCall: mockRuntimeEnvironmentCall,
|
|
runtimeTransportCall: mockRuntimeEnvironmentTransportCall,
|
|
runtimeSubscribe: mockRuntimeEnvironmentSubscribe,
|
|
subscribeToData: mockSubscribeToPtyData,
|
|
subscribeToExit: mockSubscribeToPtyExit,
|
|
setTabLayout: mockSetTabLayout,
|
|
updateTabPtyId: mockUpdateTabPtyId,
|
|
dispatchEvent: mockDispatchEvent,
|
|
kill: mockKill,
|
|
spawn: mockSpawn,
|
|
write: mockWrite
|
|
})
|
|
})
|
|
|
|
it.each(['claude', 'codex'] as const)(
|
|
'delivers independent %s effort through the real prepared background callback',
|
|
async (agent) => {
|
|
useRemoteAgentBackgroundRuntime(state)
|
|
const plan = independentActionEffortPlan(agent)
|
|
const spawn = vi.fn<Parameters<typeof consumePreparedBackgroundCommand>[1]>(async () => ({
|
|
handle: 'prepared-background',
|
|
worktreeId: 'wt-1',
|
|
title: null,
|
|
savedStartupCwd: 'private'
|
|
}))
|
|
mockRuntimeEnvironmentCall.mockImplementation(async (request) => {
|
|
if (request.method !== 'terminal.create') {
|
|
throw new Error(`Unexpected create route ${request.method}`)
|
|
}
|
|
return { ok: true, result: await consumePreparedBackgroundCommand(request.params, spawn) }
|
|
})
|
|
await expect(
|
|
createRuntimeAgentBackgroundTerminal({
|
|
environmentId: 'env-1',
|
|
worktreeId: 'wt-1',
|
|
tabId: 'reserved-tab',
|
|
leafId: 'reserved-leaf',
|
|
agent,
|
|
prompt: 'Fix checks',
|
|
sessionOptions: plan.sessionOptions,
|
|
legacy: {
|
|
command: plan.launchCommand,
|
|
env: plan.env ?? {},
|
|
launchConfig: plan.launchConfig,
|
|
startupCommandDelivery: plan.startupCommandDelivery,
|
|
launchToken: 'reserved-token'
|
|
}
|
|
})
|
|
).resolves.toEqual({
|
|
terminal: { handle: 'prepared-background', worktreeId: 'wt-1', title: null }
|
|
})
|
|
expect(spawn).toHaveBeenCalledExactlyOnceWith('id:wt-1', {
|
|
// A paired client's terminal.create is the user's act (E9).
|
|
wslUserAct: true,
|
|
command: plan.launchCommand,
|
|
env: { ACTION_PROFILE: 'captured' },
|
|
envToDelete: undefined,
|
|
launchConfig: plan.launchConfig,
|
|
launchAgent: agent,
|
|
startupCommandDelivery: agent === 'codex' ? 'shell-ready' : undefined,
|
|
terminalImageViewerSupported: false,
|
|
terminalKittyKeyboardProtocol: true,
|
|
title: undefined,
|
|
focus: false,
|
|
activate: false,
|
|
presentation: 'background',
|
|
tabId: 'reserved-tab',
|
|
leafId: 'reserved-leaf',
|
|
launchToken: 'reserved-token',
|
|
preAllocatedHandle: 'reserved-handle'
|
|
})
|
|
expect(mockSpawn).not.toHaveBeenCalled()
|
|
expect(mockRuntimeEnvironmentCall).toHaveBeenCalledExactlyOnceWith(
|
|
expect.objectContaining({
|
|
method: 'terminal.create',
|
|
params: expect.objectContaining({
|
|
tabId: 'reserved-tab',
|
|
leafId: 'reserved-leaf',
|
|
launchToken: 'reserved-token',
|
|
presentation: 'background'
|
|
})
|
|
})
|
|
)
|
|
}
|
|
)
|
|
|
|
it('closes a runtime terminal when its worktree disappears before creation resolves', async () => {
|
|
useRemoteAgentBackgroundRuntime(state)
|
|
let resolveCreate!: (result: {
|
|
ok: true
|
|
result: { terminal: { handle: string; worktreeId: string; title: null } }
|
|
}) => void
|
|
const createResult = new Promise<{
|
|
ok: true
|
|
result: { terminal: { handle: string; worktreeId: string; title: null } }
|
|
}>((resolve) => {
|
|
resolveCreate = resolve
|
|
})
|
|
mockRuntimeEnvironmentCall.mockImplementation((args: { method: string }) => {
|
|
if (args.method === 'terminal.createAgentSession') {
|
|
return createResult
|
|
}
|
|
return Promise.resolve({ ok: true, result: {} })
|
|
})
|
|
const { launchAgentBackgroundSession } =
|
|
await import('../../src/renderer/src/lib/launch-agent-background-session')
|
|
|
|
const launch = launchAgentBackgroundSession({
|
|
agent: 'claude',
|
|
worktreeId: 'wt-1',
|
|
prompt: 'run remotely'
|
|
})
|
|
await vi.waitFor(() =>
|
|
expect(mockRuntimeEnvironmentCall).toHaveBeenCalledWith(
|
|
expect.objectContaining({ method: 'terminal.createAgentSession' })
|
|
)
|
|
)
|
|
state.worktreesByRepo['repo-1'] = []
|
|
resolveCreate({
|
|
ok: true,
|
|
result: { terminal: { handle: 'terminal-after-close', worktreeId: 'wt-1', title: null } }
|
|
})
|
|
|
|
await expect(launch).resolves.toBeNull()
|
|
expect(mockRuntimeEnvironmentCall).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
method: 'terminal.close',
|
|
params: { terminal: 'terminal-after-close' }
|
|
})
|
|
)
|
|
expect(mockCreateTab).not.toHaveBeenCalled()
|
|
expect(mockUpdateTabPtyId).not.toHaveBeenCalled()
|
|
expect(mockRuntimeEnvironmentSubscribe).not.toHaveBeenCalled()
|
|
expect(mockDispatchEvent).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('forwards Hermes startup queries through SSH command transport', async () => {
|
|
state.repos = [{ id: 'repo-1', connectionId: 'ssh-1', path: '/repo' }]
|
|
const { launchAgentBackgroundSession } =
|
|
await import('../../src/renderer/src/lib/launch-agent-background-session')
|
|
|
|
await launchAgentBackgroundSession({
|
|
agent: 'hermes',
|
|
worktreeId: 'wt-1',
|
|
prompt: 'remote automation prompt'
|
|
})
|
|
|
|
expect(mockSpawn).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
command: expect.stringContaining('ORCA_HERMES_STARTUP_QUERY'),
|
|
connectionId: 'ssh-1',
|
|
env: expect.objectContaining({ ORCA_HERMES_STARTUP_QUERY: 'remote automation prompt' })
|
|
})
|
|
)
|
|
})
|
|
|
|
it.each(['claude', 'codex'] as const)(
|
|
'has the relay type the %s launch line instead of writing it from the renderer',
|
|
async (agent) => {
|
|
vi.useFakeTimers()
|
|
try {
|
|
state.repos = [{ id: 'repo-1', connectionId: 'ssh-1', path: '/repo' }]
|
|
const { launchAgentBackgroundSession } =
|
|
await import('../../src/renderer/src/lib/launch-agent-background-session')
|
|
|
|
await launchAgentBackgroundSession({
|
|
agent,
|
|
worktreeId: 'wt-1',
|
|
prompt: 'run the automation'
|
|
})
|
|
|
|
expect(mockSpawn.mock.calls[0]?.[0]).toEqual(
|
|
expect.objectContaining({
|
|
command: expect.stringContaining("'run the automation'"),
|
|
connectionId: 'ssh-1',
|
|
commandDelivery: 'provider',
|
|
startupCommandDelivery: 'shell-ready'
|
|
})
|
|
)
|
|
const dataSidecar = mockSubscribeToPtyData.mock.calls[0]?.[1] as (data: string) => void
|
|
dataSidecar('\x1b]777;orca-shell-ready\x07user@remote repo % ')
|
|
vi.advanceTimersByTime(20_000)
|
|
expect(mockWrite).not.toHaveBeenCalled()
|
|
} finally {
|
|
vi.useRealTimers()
|
|
}
|
|
}
|
|
)
|
|
|
|
it('has the relay type a promptless SSH background launch too', async () => {
|
|
state.repos = [{ id: 'repo-1', connectionId: 'ssh-1', path: '/repo' }]
|
|
const { launchAgentBackgroundSession } =
|
|
await import('../../src/renderer/src/lib/launch-agent-background-session')
|
|
|
|
await launchAgentBackgroundSession({ agent: 'codex', worktreeId: 'wt-1' })
|
|
|
|
expect(mockSpawn.mock.calls[0]?.[0]).toEqual(
|
|
expect.objectContaining({
|
|
commandDelivery: 'provider',
|
|
startupCommandDelivery: 'shell-ready'
|
|
})
|
|
)
|
|
})
|
|
|
|
it.each([true, false])(
|
|
'creates background sessions with negotiated keyboard support: %s',
|
|
async (keyboardSupported) => {
|
|
useRemoteAgentBackgroundRuntime(state)
|
|
mockRuntimeEnvironmentTransportCall.mockImplementation((request: { method: string }) => {
|
|
const status = createCompatibleRuntimeStatusResponseIfNeeded(request)
|
|
if (status?.ok) {
|
|
return Promise.resolve({
|
|
...status,
|
|
result: {
|
|
...status.result,
|
|
capabilities: status.result.capabilities?.filter(
|
|
(capability) =>
|
|
keyboardSupported || capability !== AGENT_SESSION_KEYBOARD_RUNTIME_CAPABILITY
|
|
)
|
|
}
|
|
})
|
|
}
|
|
return mockRuntimeEnvironmentCall(request)
|
|
})
|
|
const { launchAgentBackgroundSession } =
|
|
await import('../../src/renderer/src/lib/launch-agent-background-session')
|
|
|
|
const result = await launchAgentBackgroundSession({
|
|
agent: 'claude',
|
|
worktreeId: 'wt-1',
|
|
prompt: 'run the automation'
|
|
})
|
|
|
|
expect(mockSpawn).not.toHaveBeenCalled()
|
|
const params = mockRuntimeEnvironmentCall.mock.calls[0]?.[0]?.params
|
|
if (keyboardSupported) {
|
|
expect(params).toHaveProperty('terminalKittyKeyboardProtocol', true)
|
|
} else {
|
|
expect(params).not.toHaveProperty('terminalKittyKeyboardProtocol')
|
|
}
|
|
const leafId = params?.placement?.leafId
|
|
const tabId = params?.placement?.tabId
|
|
expect(leafId).toMatch(UUID_RE)
|
|
expect(tabId).toMatch(UUID_RE)
|
|
// Why: background launches have no explicit recipe override, so remote host settings win.
|
|
expect(params).not.toHaveProperty('agentArgs')
|
|
expect(mockRegisterAgentLaunchConfig).toHaveBeenCalledWith(
|
|
`${tabId}:${leafId}`,
|
|
{
|
|
agentCommand: "claude '--dangerously-skip-permissions'",
|
|
agentArgs: '--dangerously-skip-permissions',
|
|
agentEnv: {}
|
|
},
|
|
{
|
|
agentType: 'claude',
|
|
launchToken: expect.stringMatching(UUID_RE),
|
|
tabId,
|
|
leafId
|
|
}
|
|
)
|
|
expect(mockSetTabLayout).toHaveBeenCalledWith(
|
|
tabId,
|
|
expect.objectContaining({
|
|
root: { type: 'leaf', leafId },
|
|
activeLeafId: leafId,
|
|
ptyIdsByLeafId: { [leafId]: 'remote:env-1@@terminal-1' }
|
|
})
|
|
)
|
|
expect(mockRuntimeEnvironmentCall).toHaveBeenCalledWith({
|
|
selector: 'env-1',
|
|
method: 'terminal.createAgentSession',
|
|
params: expect.objectContaining({
|
|
clientOperationId: expect.stringMatching(/^\d{13}-[0-9a-f]{32}$/),
|
|
worktree: 'id:wt-1',
|
|
agent: 'claude',
|
|
prompt: 'run the automation',
|
|
promptDelivery: 'auto-submit',
|
|
placement: { tabId, leafId },
|
|
presentation: 'background'
|
|
}),
|
|
timeoutMs: 15_000
|
|
})
|
|
expect(mockUpdateTabPtyId).toHaveBeenCalledWith(tabId, 'remote:env-1@@terminal-1')
|
|
expect(mockRegisterEagerPtyBuffer).not.toHaveBeenCalled()
|
|
expect(mockRuntimeEnvironmentSubscribe).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
selector: 'env-1',
|
|
method: 'terminal.multiplex',
|
|
params: {}
|
|
}),
|
|
expect.any(Object)
|
|
)
|
|
expect(result).toMatchObject({
|
|
tabId,
|
|
paneKey: `${tabId}:${leafId}`,
|
|
ptyId: 'remote:env-1@@terminal-1',
|
|
terminalOwnership: null
|
|
})
|
|
}
|
|
)
|
|
|
|
it('advertises keyboard support for a background OMP launch', async () => {
|
|
useRemoteAgentBackgroundRuntime(state)
|
|
const { launchAgentBackgroundSession } =
|
|
await import('../../src/renderer/src/lib/launch-agent-background-session')
|
|
await launchAgentBackgroundSession({ agent: 'omp', worktreeId: 'wt-1', prompt: 'review' })
|
|
expect(mockRuntimeEnvironmentCall).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
method: 'terminal.createAgentSession',
|
|
params: expect.objectContaining({
|
|
agent: 'omp',
|
|
terminalKittyKeyboardProtocol: true,
|
|
presentation: 'background'
|
|
})
|
|
})
|
|
)
|
|
})
|
|
|
|
it('aborts the remote exit wait and closes the stream when run observers are released', async () => {
|
|
useRemoteAgentBackgroundRuntime(state)
|
|
const streamSend = vi.fn()
|
|
const waitUnsubscribe = vi.fn()
|
|
mockRuntimeEnvironmentSubscribe.mockImplementation(async (request, callbacks) => {
|
|
if (request.method === 'terminal.multiplex') {
|
|
queueMicrotask(() => callbacks.onResponse({ ok: true, result: { type: 'ready' } }))
|
|
return { unsubscribe: vi.fn(), sendBinary: streamSend }
|
|
}
|
|
if (request.method === 'terminal.wait') {
|
|
return { unsubscribe: waitUnsubscribe, sendBinary: vi.fn() }
|
|
}
|
|
throw new Error(`Unexpected runtime subscription: ${request.method}`)
|
|
})
|
|
const { launchAgentBackgroundSession } =
|
|
await import('../../src/renderer/src/lib/launch-agent-background-session')
|
|
|
|
const result = await launchAgentBackgroundSession({
|
|
agent: 'claude',
|
|
worktreeId: 'wt-1',
|
|
prompt: 'run the automation'
|
|
})
|
|
await vi.waitFor(() =>
|
|
expect(mockRuntimeEnvironmentSubscribe).toHaveBeenCalledWith(
|
|
expect.objectContaining({ method: 'terminal.wait' }),
|
|
expect.any(Object)
|
|
)
|
|
)
|
|
const sendsBeforeRelease = streamSend.mock.calls.length
|
|
|
|
result?.releaseRuntimeObservers()
|
|
result?.releaseRuntimeObservers()
|
|
|
|
await vi.waitFor(() => expect(waitUnsubscribe).toHaveBeenCalledOnce())
|
|
// One unsubscribe frame for the terminal's data stream.
|
|
expect(streamSend.mock.calls.length).toBe(sendsBeforeRelease + 1)
|
|
})
|
|
|
|
it('preserves the legacy background spawn on an old remote host', async () => {
|
|
useRemoteAgentBackgroundRuntime(state)
|
|
mockRuntimeEnvironmentTransportCall.mockImplementation((request: { method: string }) => {
|
|
if (request.method === 'status.get') {
|
|
return Promise.resolve({
|
|
id: 'status',
|
|
ok: true,
|
|
result: {
|
|
runtimeId: 'old-runtime',
|
|
graphStatus: 'ready',
|
|
runtimeProtocolVersion: 3,
|
|
minCompatibleRuntimeClientVersion: 2,
|
|
capabilities: []
|
|
}
|
|
})
|
|
}
|
|
return Promise.resolve({
|
|
id: 'create',
|
|
ok: true,
|
|
result: { terminal: { handle: 'legacy-terminal-1' } }
|
|
})
|
|
})
|
|
const { launchAgentBackgroundSession } =
|
|
await import('../../src/renderer/src/lib/launch-agent-background-session')
|
|
|
|
await expect(
|
|
launchAgentBackgroundSession({
|
|
agent: 'claude',
|
|
worktreeId: 'wt-1',
|
|
prompt: 'run remotely'
|
|
})
|
|
).resolves.toMatchObject({ ptyId: 'remote:env-1@@legacy-terminal-1' })
|
|
|
|
expect(mockRuntimeEnvironmentTransportCall).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
method: 'terminal.create',
|
|
params: expect.objectContaining({
|
|
worktree: 'id:wt-1',
|
|
command: "claude '--dangerously-skip-permissions' 'run remotely'",
|
|
terminalKittyKeyboardProtocol: true,
|
|
launchAgent: 'claude',
|
|
presentation: 'background'
|
|
})
|
|
})
|
|
)
|
|
})
|
|
|
|
it('closes a created runtime terminal when its data subscription fails', async () => {
|
|
useRemoteAgentBackgroundRuntime(state)
|
|
mockRuntimeEnvironmentSubscribe.mockRejectedValueOnce(new Error('subscription failed'))
|
|
const { launchAgentBackgroundSession } =
|
|
await import('../../src/renderer/src/lib/launch-agent-background-session')
|
|
|
|
await expect(
|
|
launchAgentBackgroundSession({
|
|
agent: 'claude',
|
|
worktreeId: 'wt-1',
|
|
prompt: 'run the automation'
|
|
})
|
|
).rejects.toThrow('subscription failed')
|
|
|
|
expect(
|
|
mockRuntimeEnvironmentCall.mock.calls.filter(
|
|
([request]) => request.method === 'terminal.createAgentSession'
|
|
)
|
|
).toHaveLength(1)
|
|
expect(
|
|
mockRuntimeEnvironmentCall.mock.calls.filter(
|
|
([request]) => request.method === 'terminal.create'
|
|
)
|
|
).toHaveLength(0)
|
|
expect(mockSpawn).not.toHaveBeenCalled()
|
|
expect(mockRuntimeEnvironmentCall).toHaveBeenCalledWith({
|
|
selector: 'env-1',
|
|
method: 'terminal.close',
|
|
params: { terminal: 'terminal-1' },
|
|
timeoutMs: undefined
|
|
})
|
|
const tabId = mockRuntimeEnvironmentCall.mock.calls[0]?.[0]?.params?.placement?.tabId
|
|
expect(tabId).toMatch(UUID_RE)
|
|
expect(state.clearTabPtyId).toHaveBeenCalledWith(tabId, 'remote:env-1@@terminal-1')
|
|
expect(state.clearAgentLaunchConfig).toHaveBeenCalledWith(
|
|
expect.stringMatching(new RegExp(`^${tabId}:`))
|
|
)
|
|
expect(mockCloseTab).toHaveBeenCalledWith(tabId, {
|
|
recordInteraction: false,
|
|
reason: 'cleanup'
|
|
})
|
|
expect(mockDispatchEvent).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('spawns an SSH folder-workspace automation on the owning host, not locally', async () => {
|
|
// Folder workspaces have no repo row, so launch ownership comes from their scope.
|
|
state.repos = [
|
|
{ id: 'repo-1', connectionId: 'ssh-1', path: '/srv/proj/api', projectGroupId: 'grp-1' }
|
|
]
|
|
state.projectGroups = [{ id: 'grp-1', parentGroupId: null, connectionId: 'ssh-1' }]
|
|
state.folderWorkspaces = [
|
|
{ id: 'fw-1', projectGroupId: 'grp-1', folderPath: '/srv/proj', connectionId: 'ssh-1' }
|
|
]
|
|
state.getKnownWorktreeById = (worktreeId: string) =>
|
|
worktreeId === 'folder:fw-1' ? { id: 'folder:fw-1', path: '/srv/proj' } : undefined
|
|
const { launchAgentBackgroundSession } =
|
|
await import('../../src/renderer/src/lib/launch-agent-background-session')
|
|
|
|
await launchAgentBackgroundSession({
|
|
agent: 'codex',
|
|
worktreeId: 'folder:fw-1',
|
|
prompt: 'run the automation'
|
|
})
|
|
|
|
expect(mockSpawn).toHaveBeenCalledWith(
|
|
expect.objectContaining({ connectionId: 'ssh-1', cwd: '/srv/proj' })
|
|
)
|
|
})
|
|
|
|
it('keeps a local folder workspace on the local host', async () => {
|
|
state.repos = [
|
|
{ id: 'repo-1', connectionId: null, path: '/home/me/proj/api', projectGroupId: 'grp-1' }
|
|
]
|
|
state.projectGroups = [{ id: 'grp-1', parentGroupId: null, connectionId: null }]
|
|
state.folderWorkspaces = [
|
|
{ id: 'fw-1', projectGroupId: 'grp-1', folderPath: '/home/me/proj', connectionId: null }
|
|
]
|
|
state.getKnownWorktreeById = (worktreeId: string) =>
|
|
worktreeId === 'folder:fw-1' ? { id: 'folder:fw-1', path: '/home/me/proj' } : undefined
|
|
const { launchAgentBackgroundSession } =
|
|
await import('../../src/renderer/src/lib/launch-agent-background-session')
|
|
|
|
await launchAgentBackgroundSession({
|
|
agent: 'claude',
|
|
worktreeId: 'folder:fw-1',
|
|
prompt: 'run the automation'
|
|
})
|
|
|
|
expect(mockSpawn).toHaveBeenCalledWith(
|
|
expect.objectContaining({ connectionId: null, cwd: '/home/me/proj' })
|
|
)
|
|
})
|
|
})
|