mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 15:46:22 +00:00
* ci: run browseros tests on pull requests * refactor: rework 0320-github_action_for_tests based on feedback * refactor: rework 0320-github_action_for_tests based on feedback * chore: add CI artifacts to .gitignore Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove mikepenz/action-junit-report to fix check suite misattribution The JUnit report action creates check runs that GitHub associates with the CLA check suite instead of the Tests check suite, causing test reports to appear under "CLA Assistant" in the PR checks UI. Remove the action and rely on job status + step summary + artifact upload for test result visibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
160 lines
3.9 KiB
TypeScript
160 lines
3.9 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 BrowserOS
|
|
*
|
|
* Low-level BrowserOS process management.
|
|
* Use setup.ts:ensureBrowserOS() for the full test environment.
|
|
*/
|
|
import type { ChildProcess } from 'node:child_process'
|
|
import { spawn } from 'node:child_process'
|
|
import { rmSync } from 'node:fs'
|
|
|
|
export interface BrowserConfig {
|
|
cdpPort: number
|
|
serverPort: number
|
|
extensionPort: number
|
|
binaryPath: string
|
|
userDataDir: string
|
|
headless: boolean
|
|
extraArgs: string[]
|
|
}
|
|
|
|
interface BrowserState {
|
|
process: ChildProcess
|
|
userDataDir: string
|
|
config: BrowserConfig
|
|
}
|
|
|
|
let browserState: BrowserState | null = null
|
|
|
|
function shouldLogBrowserOutput(): boolean {
|
|
return (
|
|
process.env.CI === 'true' || process.env.BROWSEROS_TEST_DEBUG === 'true'
|
|
)
|
|
}
|
|
|
|
export async function isBrowserRunning(cdpPort: number): Promise<boolean> {
|
|
try {
|
|
const response = await fetch(`http://127.0.0.1:${cdpPort}/json/version`, {
|
|
signal: AbortSignal.timeout(1000),
|
|
})
|
|
return response.ok
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function waitForCdp(cdpPort: number, maxAttempts = 30): Promise<void> {
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
if (await isBrowserRunning(cdpPort)) {
|
|
return
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
}
|
|
throw new Error(`CDP failed to start on port ${cdpPort} within timeout`)
|
|
}
|
|
|
|
export function getBrowserState(): BrowserState | null {
|
|
return browserState
|
|
}
|
|
|
|
export async function spawnBrowser(
|
|
config: BrowserConfig,
|
|
): Promise<BrowserState> {
|
|
if (browserState && browserState.config.cdpPort === config.cdpPort) {
|
|
if (await isBrowserRunning(config.cdpPort)) {
|
|
console.log(`Reusing existing browser on CDP port ${config.cdpPort}`)
|
|
return browserState
|
|
}
|
|
}
|
|
|
|
if (browserState) {
|
|
console.log('Config changed, cleaning up existing browser...')
|
|
await killBrowser()
|
|
}
|
|
|
|
console.log(`Starting BrowserOS on CDP port ${config.cdpPort}...`)
|
|
const browserProcess = spawn(
|
|
config.binaryPath,
|
|
[
|
|
'--use-mock-keychain',
|
|
'--show-component-extension-options',
|
|
'--enable-logging=stderr',
|
|
...(config.headless ? ['--headless=new'] : []),
|
|
...config.extraArgs,
|
|
`--user-data-dir=${config.userDataDir}`,
|
|
// TODO: replace with --browseros-cdp-port once we fix the browseros bug
|
|
`--remote-debugging-port=${config.cdpPort}`,
|
|
`--browseros-mcp-port=${config.serverPort}`,
|
|
`--browseros-extension-port=${config.extensionPort}`,
|
|
'--disable-browseros-server',
|
|
],
|
|
{
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
},
|
|
)
|
|
|
|
browserProcess.stdout?.on('data', (data) => {
|
|
if (!shouldLogBrowserOutput()) {
|
|
return
|
|
}
|
|
console.log(`[BROWSER] ${data.toString().trim()}`)
|
|
})
|
|
|
|
browserProcess.stderr?.on('data', (data) => {
|
|
if (!shouldLogBrowserOutput()) {
|
|
return
|
|
}
|
|
console.error(`[BROWSER] ${data.toString().trim()}`)
|
|
})
|
|
|
|
browserProcess.on('error', (error) => {
|
|
console.error('Failed to start BrowserOS:', error)
|
|
})
|
|
|
|
console.log('Waiting for CDP to be ready...')
|
|
await waitForCdp(config.cdpPort)
|
|
console.log('CDP is ready')
|
|
|
|
browserState = {
|
|
process: browserProcess,
|
|
userDataDir: config.userDataDir,
|
|
config,
|
|
}
|
|
return browserState
|
|
}
|
|
|
|
export async function killBrowser(): Promise<void> {
|
|
if (!browserState) {
|
|
return
|
|
}
|
|
|
|
console.log('Shutting down BrowserOS...')
|
|
browserState.process.kill('SIGTERM')
|
|
|
|
await new Promise<void>((resolve) => {
|
|
const timeout = setTimeout(() => {
|
|
browserState?.process.kill('SIGKILL')
|
|
resolve()
|
|
}, 5000)
|
|
|
|
browserState?.process.on('exit', () => {
|
|
clearTimeout(timeout)
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
console.log('BrowserOS stopped')
|
|
|
|
if (browserState.userDataDir) {
|
|
console.log(`Cleaning up temp profile: ${browserState.userDataDir}`)
|
|
try {
|
|
rmSync(browserState.userDataDir, { recursive: true, force: true })
|
|
} catch (error) {
|
|
console.error('Failed to clean up temp directory:', error)
|
|
}
|
|
}
|
|
|
|
browserState = null
|
|
}
|