// built-in skill: run Maestro tests against the app import type { RNXSkill } from '../types' function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } export const skill: RNXSkill = { name: 'maestro-test', description: 'Run Maestro YAML tests against the app', type: 'testing', version: '1.0.0', triggers: ['run test', 'test flow', 'run maestro', 'test the app'], tools: [ { name: 'run_maestro', description: 'Run a Maestro YAML test against the app', parameters: { flowPath: { type: 'string', description: 'Path to the YAML flow file', required: true, }, record: { type: 'boolean', description: 'Record video of the flow' }, }, async execute(params, context) { const fs = await import('fs') const path = await import('path') const yaml = await import('yaml') const { DEFAULT_SOOTSIM_BRIDGE_PORT } = await import('../../bridge-constants') const { SootSimBridgeHost } = await import('../../host/bridge-host') const { createBridge } = await import('../../../cli/ws-bridge') const { runOpenCommand } = await import('../../../cli/commands/control') const { SootSimBridgeFlowRunner } = await import('../../../cli/bridge-flow-runner') const host = new SootSimBridgeHost({ port: DEFAULT_SOOTSIM_BRIDGE_PORT }) host.start({ silent: true }) try { const flowContent = fs.readFileSync(params.flowPath, 'utf8') const fmMatch = flowContent.match(/^---\n([\s\S]*?)\n---\n?/) const stepsBody = fmMatch ? flowContent.slice(fmMatch[0].length) : flowContent const steps = yaml.parse(stepsBody) const beforeIds = new Set(host.listSims().map((sim) => sim.id)) await runOpenCommand([context.url, '--new', '--headless', '--no-describe'], { port: DEFAULT_SOOTSIM_BRIDGE_PORT, }) let simId: string | null = null for (let i = 0; i < 60; i++) { const sims = host.listSims() const opened = sims.find((sim) => !beforeIds.has(sim.id) && sim.readyState === 'open') || sims.find((sim) => sim.readyState === 'open') if (opened) { simId = opened.id break } await sleep(500) } if (!simId) { throw new Error('no sim connected to the bridge host') } const bridge = createBridge(DEFAULT_SOOTSIM_BRIDGE_PORT, { simId, commandTimeoutMs: 15000, }) const driver = new SootSimBridgeFlowRunner(bridge, { screenshotDir: context.outputDir, flowDir: path.dirname(path.resolve(params.flowPath)), simId, recordingOutputDir: params.record ? context.outputDir : undefined, }) let videoPath: string | null = null try { await driver.waitForTree(120000) if (params.record) { await driver.startRecording() } await driver.runFlow(steps) if (params.record) { videoPath = await driver.stopRecording() } return { success: true, message: `Flow completed: ${steps.length} steps passed`, data: { steps: steps.length, simId, videoPath: videoPath || undefined, }, artifacts: videoPath ? [{ type: 'video', name: path.basename(videoPath), path: videoPath }] : undefined, } } finally { try { await bridge.closeSim(simId) } catch {} bridge.close() } } catch (err: any) { return { success: false, message: `Flow failed: ${err.message}`, } } finally { await host.close().catch(() => {}) } }, }, ], }