From b4ce58ba85b238858193abec57e17655b4e2f7bc Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Wed, 4 Nov 2026 10:00:25 +0800 Subject: [PATCH 1/2] fix(recipes): select native video recorders --- scripts/agentic/package.json | 3 ++- scripts/agentic/recipe/provider.mjs | 13 ++++++++-- scripts/agentic/recipe/provider.test.mjs | 24 ++++++++++++++++++- yarn.lock | 30 ++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/scripts/agentic/package.json b/scripts/agentic/package.json index b27814119..713edaf47 100644 --- a/scripts/agentic/package.json +++ b/scripts/agentic/package.json @@ -5,6 +5,7 @@ "@farmslot/adapter-sdk": "0.25.2", "@farmslot/recipe-cli": "0.32.1", "@farmslot/recipe-runner": "0.46.2", - "pngjs": "^7.0.0" + "pngjs": "^7.0.0", + "@farmslot/adapter-rn": "0.29.2" } } diff --git a/scripts/agentic/recipe/provider.mjs b/scripts/agentic/recipe/provider.mjs index 4b6bed608..90de3448a 100644 --- a/scripts/agentic/recipe/provider.mjs +++ b/scripts/agentic/recipe/provider.mjs @@ -1,7 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; import { ADAPTER_SDK_VERSION } from '@farmslot/adapter-sdk'; -import { createCdpVideoRecorder } from '@farmslot/recipe-runner'; +import { createAndroidMirrorVideoRecorder, createCdpVideoRecorder } from '@farmslot/recipe-runner'; +import { createAndroidVideoRecorder, createIosSimulatorVideoRecorder } from '@farmslot/adapter-rn'; import { contextPorts } from '@farmslot/recipe-cli/harness'; import { recordingTarget } from './recording-target.mjs'; import { runOwnedRecipeProcess, recipeExecutionSignal } from '@farmslot/recipe-runner/adapters/core'; @@ -165,7 +166,15 @@ export function createProvider(context) { })), }, recording: { target: () => recordingTarget({ platform, serial, simulator, device: explicitDevice, iosDevice, cdp, metro }), - ...(platform === 'web' ? { videoRecorder: async () => createCdpVideoRecorder({ cdpPort: cdp, urlIncludes: `:${metro}` }) } : {}), + videoRecorder: async () => { + if (platform === 'web') return createCdpVideoRecorder({ cdpPort: cdp, urlIncludes: `:${metro}` }); + if (platform === 'android') { + const fallback = createAndroidVideoRecorder(serial); + return serial.startsWith('emulator-') ? fallback : createAndroidMirrorVideoRecorder({ serial, fallback }); + } + if (!iosDevice && !explicitDevice?.startsWith('device:')) + return createIosSimulatorVideoRecorder(simulator || explicitDevice); + }, }, }, cancel() { if (launchedMetro) stopMetro(appRoot, metro); }, diff --git a/scripts/agentic/recipe/provider.test.mjs b/scripts/agentic/recipe/provider.test.mjs index 589dddc62..e011a7c10 100644 --- a/scripts/agentic/recipe/provider.test.mjs +++ b/scripts/agentic/recipe/provider.test.mjs @@ -213,10 +213,32 @@ test('web recording selects the shared CDP recorder and binds its assigned endpo outputPath: path.join(os.tmpdir(), 'audiolab-endpoint-refusal.mp4'), }), /different endpoint/); const native = createProvider(context('playground', 'ios')); - assert.equal(native.runtime.recording.videoRecorder, undefined); + assert.equal((await native.runtime.recording.videoRecorder()).name, 'simctl'); assert.deepEqual(await native.runtime.recording.target(), { kind: 'simulator', device: 'assigned-simulator' }); }); +test('native recording uses the shared recorder bound to the assigned target', async t => { + const saved = process.env.ADB_SERIAL; + t.after(() => { if (saved === undefined) delete process.env.ADB_SERIAL;else process.env.ADB_SERIAL = saved; }); + for (const [serial, expected] of [['assigned-physical-device', 'android-recording'], ['emulator-5554', 'adb-screenrecord']]) { + process.env.ADB_SERIAL = serial; + const recording = createProvider(context('playground', 'android')).runtime.recording; + assert.equal(typeof recording.videoRecorder, 'function'); + const recorder = await recording.videoRecorder(); + assert.equal(recorder.name, expected); + assert.deepEqual(await recording.target(), { kind: 'android-device', serial }); + await assert.rejects(recorder.start({ target: { kind: 'android-device', serial: 'foreign-device' }, outputPath: '/unused.mp4' }), /does not match/); + } + const recording = createProvider(context('playground', 'ios')).runtime.recording; + assert.equal(typeof recording.videoRecorder, 'function'); + assert.equal((await recording.videoRecorder()).name, 'simctl'); + const physical = context('playground', 'ios'); + physical.options.device = 'device:assigned-iphone'; + const unsupported = createProvider(physical).runtime.recording; + assert.equal(await unsupported.videoRecorder(), undefined); + await assert.rejects(unsupported.target(), /Physical iOS video capture is not declared/); +}); + test('built-in verification observes the bound bridge and refuses missing route readiness', async t => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'audiolab-provider-verify-')); t.after(() => fs.rm(root, { recursive: true, force: true })); diff --git a/yarn.lock b/yarn.lock index 9550541f2..7558b1c74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5136,6 +5136,35 @@ __metadata: languageName: node linkType: hard +"@farmslot/adapter-rn@npm:0.29.2": + version: 0.29.2 + resolution: "@farmslot/adapter-rn@npm:0.29.2" + dependencies: + "@farmslot/agent-runtime": "npm:0.39.8" + "@farmslot/protocol": "npm:0.74.0" + "@farmslot/recipe-runner": "npm:0.46.2" + pngjs: "npm:^7.0.0" + ws: "npm:^8.18.0" + peerDependencies: + agent-device: ">=0.19.3" + react: ">=19" + react-native: ">=0.79" + react-native-safe-area-context: ">=5" + peerDependenciesMeta: + agent-device: + optional: true + react: + optional: true + react-native: + optional: true + react-native-safe-area-context: + optional: true + bin: + farmslot-adapter-rn: ./bin/farmslot-adapter-rn.mjs + checksum: 10/7cd64667d767185cfae24148da4de5174720b65de9fa9a020617df2d97c9cbabd2ae4a56678ce95edead2511c299c6c4f988e9900003c0c27b7486f55fbb0b03 + languageName: node + linkType: hard + "@farmslot/adapter-sdk@npm:0.25.2": version: 0.25.2 resolution: "@farmslot/adapter-sdk@npm:0.25.2" @@ -8749,6 +8778,7 @@ __metadata: version: 0.0.0-use.local resolution: "@siteed/audiolab-agentic@workspace:scripts/agentic" dependencies: + "@farmslot/adapter-rn": "npm:0.29.2" "@farmslot/adapter-sdk": "npm:0.25.2" "@farmslot/recipe-cli": "npm:0.32.1" "@farmslot/recipe-runner": "npm:0.46.2" From e9e8940ab9df6d5a56598f423c63dd85aef9e7be Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Wed, 4 Nov 2026 10:37:31 +0800 Subject: [PATCH 2/2] fix(recipes): use the Expo Android recording backend --- scripts/agentic/recipe/provider.mjs | 7 ++----- scripts/agentic/recipe/provider.test.mjs | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/scripts/agentic/recipe/provider.mjs b/scripts/agentic/recipe/provider.mjs index 90de3448a..daa256933 100644 --- a/scripts/agentic/recipe/provider.mjs +++ b/scripts/agentic/recipe/provider.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { ADAPTER_SDK_VERSION } from '@farmslot/adapter-sdk'; -import { createAndroidMirrorVideoRecorder, createCdpVideoRecorder } from '@farmslot/recipe-runner'; +import { createCdpVideoRecorder } from '@farmslot/recipe-runner'; import { createAndroidVideoRecorder, createIosSimulatorVideoRecorder } from '@farmslot/adapter-rn'; import { contextPorts } from '@farmslot/recipe-cli/harness'; import { recordingTarget } from './recording-target.mjs'; @@ -168,10 +168,7 @@ export function createProvider(context) { recording: { target: () => recordingTarget({ platform, serial, simulator, device: explicitDevice, iosDevice, cdp, metro }), videoRecorder: async () => { if (platform === 'web') return createCdpVideoRecorder({ cdpPort: cdp, urlIncludes: `:${metro}` }); - if (platform === 'android') { - const fallback = createAndroidVideoRecorder(serial); - return serial.startsWith('emulator-') ? fallback : createAndroidMirrorVideoRecorder({ serial, fallback }); - } + if (platform === 'android') return createAndroidVideoRecorder(serial); if (!iosDevice && !explicitDevice?.startsWith('device:')) return createIosSimulatorVideoRecorder(simulator || explicitDevice); }, diff --git a/scripts/agentic/recipe/provider.test.mjs b/scripts/agentic/recipe/provider.test.mjs index e011a7c10..29d9f0982 100644 --- a/scripts/agentic/recipe/provider.test.mjs +++ b/scripts/agentic/recipe/provider.test.mjs @@ -220,7 +220,7 @@ test('web recording selects the shared CDP recorder and binds its assigned endpo test('native recording uses the shared recorder bound to the assigned target', async t => { const saved = process.env.ADB_SERIAL; t.after(() => { if (saved === undefined) delete process.env.ADB_SERIAL;else process.env.ADB_SERIAL = saved; }); - for (const [serial, expected] of [['assigned-physical-device', 'android-recording'], ['emulator-5554', 'adb-screenrecord']]) { + for (const [serial, expected] of [['assigned-physical-device', 'adb-screenrecord'], ['emulator-5554', 'adb-screenrecord']]) { process.env.ADB_SERIAL = serial; const recording = createProvider(context('playground', 'android')).runtime.recording; assert.equal(typeof recording.videoRecorder, 'function');