Compare commits

...

2 Commits

Author SHA1 Message Date
Nikhil Sonti
e78bf58d41 test: cover terminal limactl resolver errors 2026-04-28 17:04:39 -07:00
Nikhil Sonti
a1c83a4b9c fix: avoid eager limactl resolution in server tests 2026-04-28 16:53:12 -07:00
7 changed files with 148 additions and 15 deletions

View File

@@ -54,6 +54,10 @@ jobs:
command: (cd apps/server && bun run test:integration)
junit_path: test-results/server-integration.xml
needs_browser: true
- suite: server-lib
command: (cd apps/server && bun run test:lib)
junit_path: test-results/server-lib.xml
needs_browser: false
- suite: server-sdk
command: (cd apps/server && bun run test:sdk)
junit_path: test-results/server-sdk.xml

View File

@@ -18,6 +18,7 @@
"test:cdp": "bun run test:browser",
"test:core": "bun run ./tests/__helpers__/run-test-group.ts core",
"test:integration": "bun run ./tests/__helpers__/run-test-group.ts integration",
"test:lib": "bun run ./tests/__helpers__/run-test-group.ts lib",
"test:root": "bun run ./tests/__helpers__/run-test-group.ts root",
"test:sdk": "bun run ./tests/__helpers__/run-test-group.ts sdk",
"test:skills": "bun run ./tests/__helpers__/run-test-group.ts skills",

View File

@@ -17,7 +17,7 @@ export const TERMINAL_WS_PATH = '/terminal/ws'
interface TerminalRouteDeps {
containerName: string
limaHome: string
limactlPath: string
limactlPath: string | (() => string)
vmName: string
}
@@ -39,16 +39,20 @@ function sendExit(ws: { send(data: string): void }, exitCode: number): void {
safeSend(ws, serializeTerminalServerMessage({ type: 'exit', exitCode }))
}
function createSocketEvents(deps: TerminalRouteDeps) {
export function createTerminalSocketEvents(deps: TerminalRouteDeps) {
let session: TerminalSession | null = null
return {
onOpen(_event: Event, ws: { send(data: string): void; close(): void }) {
try {
const limactlPath =
typeof deps.limactlPath === 'function'
? deps.limactlPath()
: deps.limactlPath
session = createTerminalSession({
containerName: deps.containerName,
limaHome: deps.limaHome,
limactlPath: deps.limactlPath,
limactlPath,
vmName: deps.vmName,
workingDir: TERMINAL_HOME_DIR,
onOutput(data) {
@@ -93,6 +97,6 @@ function createSocketEvents(deps: TerminalRouteDeps) {
export function createTerminalRoutes(deps: TerminalRouteDeps) {
return new Hono<Env>().get(
'/ws',
upgradeWebSocket(() => createSocketEvents(deps)),
upgradeWebSocket(() => createTerminalSocketEvents(deps)),
)
}

View File

@@ -116,7 +116,7 @@ export async function createHttpServer(config: HttpServerConfig) {
createTerminalRoutes({
containerName: OPENCLAW_GATEWAY_CONTAINER_NAME,
limaHome: getLimaHomeDir(),
limactlPath: resolveBundledLimactl(resourcesDir),
limactlPath: () => resolveBundledLimactl(resourcesDir),
vmName: VM_NAME,
}),
)

View File

@@ -48,15 +48,15 @@ function listRootTestTargets(): string[] {
.sort((left, right) => left.localeCompare(right))
}
function listAllGroups(): string[] {
const groups = [...listDirectoryGroups()]
export function listAllGroups(): string[] {
const groups = new Set(listDirectoryGroups())
if (existsSync(resolve(testsRoot, 'server.integration.test.ts'))) {
groups.push('integration')
groups.add('integration')
}
if (listRootTestTargets().length > 0) {
groups.push('root')
groups.add('root')
}
return groups
return [...groups]
}
function listAvailableGroupNames(): string[] {
@@ -75,14 +75,19 @@ function getCompositeGroupMembers(group: string): string[] | null {
return null
}
function getAtomicGroupTargets(group: string): string[] {
export function getAtomicGroupTargets(group: string): string[] {
if (group === 'cdp') {
return getAtomicGroupTargets('browser')
}
if (group === 'integration') {
return existsSync(resolve(testsRoot, 'server.integration.test.ts'))
? ['./tests/server.integration.test.ts']
: []
const targets: string[] = []
if (existsSync(resolve(testsRoot, 'integration'))) {
targets.push('./tests/integration')
}
if (existsSync(resolve(testsRoot, 'server.integration.test.ts'))) {
targets.push('./tests/server.integration.test.ts')
}
return targets
}
if (group === 'root') {
return listRootTestTargets()

View File

@@ -0,0 +1,95 @@
/**
* @license
* Copyright 2025 BrowserOS
*/
import { afterEach, describe, expect, it, mock } from 'bun:test'
describe('createTerminalSocketEvents', () => {
afterEach(() => {
mock.restore()
})
it('resolves limactl only when a terminal socket opens', async () => {
const close = mock(() => {})
const send = mock(() => {})
const session = {
close: mock(() => {}),
resize: mock(() => {}),
writeInput: mock(() => {}),
}
const createTerminalSession = mock(() => session)
const actualTerminalSession = await import(
'../../../src/api/services/terminal/terminal-session'
)
mock.module('../../../src/api/services/terminal/terminal-session', () => ({
...actualTerminalSession,
createTerminalSession,
}))
const { createTerminalSocketEvents } = await import(
'../../../src/api/routes/terminal'
)
const resolveLimactlPath = mock(() => '/tmp/fake-limactl')
const events = createTerminalSocketEvents({
containerName: 'gateway',
limaHome: '/tmp/lima',
limactlPath: resolveLimactlPath,
vmName: 'browseros-vm',
})
expect(resolveLimactlPath).not.toHaveBeenCalled()
events.onOpen(new Event('open'), { send, close })
expect(resolveLimactlPath).toHaveBeenCalledTimes(1)
expect(createTerminalSession).toHaveBeenCalledWith(
expect.objectContaining({
containerName: 'gateway',
limaHome: '/tmp/lima',
limactlPath: '/tmp/fake-limactl',
vmName: 'browseros-vm',
workingDir: actualTerminalSession.TERMINAL_HOME_DIR,
}),
)
expect(close).not.toHaveBeenCalled()
})
it('sends an error and closes when the limactl resolver throws', async () => {
const close = mock(() => {})
const send = mock(() => {})
const createTerminalSession = mock(() => {
throw new Error('should not start a session')
})
const actualTerminalSession = await import(
'../../../src/api/services/terminal/terminal-session'
)
mock.module('../../../src/api/services/terminal/terminal-session', () => ({
...actualTerminalSession,
createTerminalSession,
}))
const { createTerminalSocketEvents } = await import(
'../../../src/api/routes/terminal'
)
const events = createTerminalSocketEvents({
containerName: 'gateway',
limaHome: '/tmp/lima',
limactlPath: () => {
throw new Error('limactl not found')
},
vmName: 'browseros-vm',
})
events.onOpen(new Event('open'), { send, close })
expect(createTerminalSession).not.toHaveBeenCalled()
expect(send).toHaveBeenCalledWith(
JSON.stringify({ type: 'error', message: 'limactl not found' }),
)
expect(close).toHaveBeenCalledTimes(1)
})
})

View File

@@ -4,7 +4,12 @@
*/
import { describe, expect, it } from 'bun:test'
import { buildTestCommand, withTestEnv } from './__helpers__/run-test-group'
import {
buildTestCommand,
getAtomicGroupTargets,
listAllGroups,
withTestEnv,
} from './__helpers__/run-test-group'
describe('withTestEnv', () => {
it('defaults NODE_ENV to test when absent', () => {
@@ -27,3 +32,22 @@ describe('buildTestCommand', () => {
])
})
})
describe('test groups', () => {
it('includes the lib tests in the group list', () => {
expect(listAllGroups()).toContain('lib')
})
it('runs root and directory integration tests in the integration group', () => {
expect(getAtomicGroupTargets('integration')).toEqual([
'./tests/integration',
'./tests/server.integration.test.ts',
])
})
it('does not duplicate group names', () => {
const groups = listAllGroups()
expect(new Set(groups).size).toBe(groups.length)
})
})