Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
"dependencies": {
"@agent-relay/cloud": "^9.1.6",
"@agent-relay/factory": "^0.1.13",
"@agent-relay/fleet": "^9.1.6",
"@agent-relay/harness-driver": "^9.1.6",
"@agent-relay/harnesses": "^9.1.6",
"@agent-relay/integration-prompts": "^9.1.6",
"@agent-relay/sdk": "^9.1.6",
"@agentworkforce/deploy": "^4.0.2",
Expand All @@ -61,6 +63,7 @@
"shiki": "^4.0.2",
"unidiff": "^1.0.4",
"use-stick-to-bottom": "^1.1.6",
"ws": "^8.21.0",
"zod": "^3.25.76",
"zustand": "^5.0.0"
},
Expand All @@ -74,6 +77,7 @@
"@testing-library/react": "^16.3.2",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/parser": "^8.61.0",
"@vitejs/plugin-react": "^4.3.0",
Expand Down
125 changes: 124 additions & 1 deletion src/main/broker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ const mock = vi.hoisted(() => {
}
return { event_id: eventId, targets: target && !target.startsWith('#') ? [target] : [] }
}),
brokerPid: 4242
brokerPid: 4242,
baseUrl: 'http://127.0.0.1:4242'
}
return client
}
Expand Down Expand Up @@ -154,6 +155,35 @@ const mock = vi.hoisted(() => {
return { state, createMockClient, HarnessDriverClient }
})

const fleetNodeMock = vi.hoisted(() => {
type SidecarMock = { registered: Promise<unknown>; done: Promise<void>; stop: ReturnType<typeof vi.fn> }
const sidecars: SidecarMock[] = []
const startPearFleetSidecar = vi.fn((_options: unknown): SidecarMock => {
const sidecar: SidecarMock = {
registered: Promise.resolve({
name: 'pear-project-1-local-fleet',
capabilities: [
'claude',
'codex',
'gemini',
'opencode',
'grok',
'aider',
'goose',
'cursor',
'droid'
].map((cli) => ({ name: `spawn:${cli}`, kind: 'action' }))
}),
done: new Promise<void>(() => undefined),
stop: vi.fn(async () => undefined)
}
sidecars.push(sidecar)
return sidecar
})

return { sidecars, startPearFleetSidecar }
})

const electronMock = vi.hoisted(() => ({
app: {
getAppPath: () => '/tmp/pear-app',
Expand All @@ -171,6 +201,10 @@ vi.mock('@agent-relay/harness-driver', () => ({
HarnessDriverClient: mock.HarnessDriverClient
}))

vi.mock('./pear-fleet-node', () => ({
startPearFleetSidecar: fleetNodeMock.startPearFleetSidecar
}))

vi.mock('./auth', () => ({
getAccessToken: vi.fn(async () => 'token'),
getApiUrl: vi.fn(() => 'https://cloud.example')
Expand Down Expand Up @@ -581,6 +615,8 @@ describe('BrokerManager local + cloud coexistence', () => {
mock.state.nextConnectedSessionErrors = []
mock.HarnessDriverClient.spawn.mockClear()
mock.HarnessDriverClient.connect.mockClear()
fleetNodeMock.sidecars.length = 0
fleetNodeMock.startPearFleetSidecar.mockClear()
})

afterEach(async () => {
Expand Down Expand Up @@ -667,6 +703,88 @@ describe('BrokerManager local + cloud coexistence', () => {
await manager.shutdown()
})

it('registers a local fleet node for spawned brokers', async () => {
const manager = new BrokerManager()

await manager.start(PROJECT_ID, '/tmp/project-1', 'pear-project-1', undefined as never, [])

expect(fleetNodeMock.startPearFleetSidecar).toHaveBeenCalledTimes(1)
expect(fleetNodeMock.startPearFleetSidecar).toHaveBeenCalledWith(expect.objectContaining({
projectId: PROJECT_ID,
cwd: '/tmp/project-1',
brokerName: 'pear-project-1',
connection: {
url: 'http://127.0.0.1:4242'
}
}))

await manager.shutdown()
expect(fleetNodeMock.sidecars[0]?.stop).toHaveBeenCalledTimes(1)
})

it('keeps one local fleet node when reusing the same broker session', async () => {
const manager = new BrokerManager()

await manager.start(PROJECT_ID, '/tmp/project-1', 'pear-project-1', undefined as never, [])
await manager.start(PROJECT_ID, '/tmp/project-1', 'pear-project-1', undefined as never, [])

expect(fleetNodeMock.startPearFleetSidecar).toHaveBeenCalledTimes(1)

await manager.shutdown()
})

it('does not register a local fleet node for cloud sandbox sessions', async () => {
const manager = new BrokerManager()

await attachCloud(manager, ['cloud-agent'])

expect(fleetNodeMock.startPearFleetSidecar).not.toHaveBeenCalled()

await manager.shutdown()
})

it('does not block local broker start when fleet registration hangs', async () => {
const sidecar = {
registered: new Promise<unknown>(() => undefined),
done: Promise.resolve(),
stop: vi.fn(async () => undefined)
}
fleetNodeMock.startPearFleetSidecar.mockImplementationOnce(() => {
fleetNodeMock.sidecars.push(sidecar)
return sidecar
})
const manager = new BrokerManager()

try {
const started = manager.start(PROJECT_ID, '/tmp/project-1', 'pear-project-1', undefined as never, [])

await expect(started).resolves.toBe(true)
expect(mock.HarnessDriverClient.spawn).toHaveBeenCalledTimes(1)
} finally {
await manager.shutdown()
}
}, 5_000)

it('does not block local broker shutdown when fleet stop hangs', async () => {
const sidecar = {
registered: Promise.resolve({ name: 'pear-project-1-local-fleet', capabilities: [] }),
done: new Promise<void>(() => undefined),
stop: vi.fn(() => new Promise<void>(() => undefined))
}
fleetNodeMock.startPearFleetSidecar.mockImplementationOnce(() => {
fleetNodeMock.sidecars.push(sidecar)
return sidecar
})
const manager = new BrokerManager()

await manager.start(PROJECT_ID, '/tmp/project-1', 'pear-project-1', undefined as never, [])
const local = lastSpawned()
const shutdown = manager.shutdown()

await expect(shutdown).resolves.toBeUndefined()
expect(local.shutdown).toHaveBeenCalled()
}, 5_000)

it('passes an explicit workspace key env pin to local broker spawn options', async () => {
const previousWorkspaceKey = process.env.AGENT_RELAY_WORKSPACE_KEY
process.env.AGENT_RELAY_WORKSPACE_KEY = 'rk_live_pinned'
Expand Down Expand Up @@ -801,6 +919,11 @@ describe('BrokerManager local + cloud coexistence', () => {
expect(mock.HarnessDriverClient.connect).toHaveBeenCalledWith({ cwd: tempDir, connectionPath })
expect(mock.HarnessDriverClient.spawn).not.toHaveBeenCalled()
expect(mock.state.connectedClients).toHaveLength(1)
expect(fleetNodeMock.startPearFleetSidecar).toHaveBeenCalledWith(expect.objectContaining({
projectId: PROJECT_ID,
cwd: tempDir,
brokerName: 'pear-project-1'
}))

await manager.shutdown()
} finally {
Expand Down
Loading
Loading