From f4f2ca52d5c128c940a1bdd8800aa14936bf8a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 1 Oct 2026 12:11:54 +0200 Subject: [PATCH] fix(provider-webdriver): an orientation refused by every route reports dispatched no --- contracts/fixtures/dispatch-disclosure.json | 6 +++ .../src/dispatch-disclosure.fixtures.ts | 1 + .../src/webdriver-orientation.test.ts | 38 +++++++++++++++++++ .../src/webdriver-orientation.ts | 3 +- .../src/webdriver-transport.ts | 14 ++++++- 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/contracts/fixtures/dispatch-disclosure.json b/contracts/fixtures/dispatch-disclosure.json index 0a9038e3df..0815e3e8ac 100644 --- a/contracts/fixtures/dispatch-disclosure.json +++ b/contracts/fixtures/dispatch-disclosure.json @@ -474,5 +474,11 @@ "producer": "webdriver", "trigger": "a mutating POST the driver answered 503: it received the request, but not whether the mutation completed", "dispatched": "unknown" + }, + { + "id": "webdriver.orientation.every-route-unsupported", + "producer": "webdriver", + "trigger": "orientation refused by both the rotation route and the orientation route: each answer said the route is not implemented, so nothing ran", + "dispatched": "no" } ] diff --git a/packages/contracts/src/dispatch-disclosure.fixtures.ts b/packages/contracts/src/dispatch-disclosure.fixtures.ts index fc6caf549f..79379b6d7e 100644 --- a/packages/contracts/src/dispatch-disclosure.fixtures.ts +++ b/packages/contracts/src/dispatch-disclosure.fixtures.ts @@ -51,6 +51,7 @@ export const DISPATCH_DISCLOSURE_DRIVER_OWNERS: Readonly> 'android-helper.gesture-session.': 'packages/platform-android/src/__tests__/touch-helper-session.test.ts', 'webdriver.': 'packages/provider-webdriver/src/webdriver-transport.test.ts', + 'webdriver.orientation.': 'packages/provider-webdriver/src/webdriver-orientation.test.ts', 'maestro-direct.': 'src/daemon/interaction/internal/__tests__/interaction-touch-direct-ios.test.ts', }; diff --git a/packages/provider-webdriver/src/webdriver-orientation.test.ts b/packages/provider-webdriver/src/webdriver-orientation.test.ts index 4195857282..9056116c88 100644 --- a/packages/provider-webdriver/src/webdriver-orientation.test.ts +++ b/packages/provider-webdriver/src/webdriver-orientation.test.ts @@ -5,6 +5,12 @@ import { AppError } from '@agent-device/kernel/errors'; import type { WebDriverClient } from './webdriver-client.ts'; import { setWebDriverOrientation } from './webdriver-orientation.ts'; import { WebDriverTransport } from './webdriver-transport.ts'; +import fs from 'node:fs'; +import { + assertDispatchDisclosureDriversMatchRows, + DISPATCH_DISCLOSURE_TABLE_PATH, + dispatchDisclosureRowsOwnedBy, +} from '@agent-device/contracts/dispatch-disclosure-fixtures'; type Call = { method: string; args: unknown[] }; @@ -181,6 +187,8 @@ test('exhausting both endpoints reports the rotation and each attempt', async () assert.ok(error instanceof AppError); assert.equal(error.code, 'COMMAND_FAILED'); assert.match(error.message, /landscape-left/); + assert.equal(error.details?.reason, 'webdriver_route_unsupported'); + assert.equal(error.details?.dispatched, 'no'); assert.match(String(error.details?.hint), /--provider-device-orientation/); const attempts = error.details?.attempts; assert.ok(Array.isArray(attempts)); @@ -192,3 +200,33 @@ test('exhausting both endpoints reports the rotation and each attempt', async () }, ); }); + +const DRIVERS: Record Promise> = { + 'webdriver.orientation.every-route-unsupported': async () => + await setWebDriverOrientation( + makeClient({ reject: ['setRotation', 'setOrientation'] }).client, + 'android', + 'landscape-left', + ), +}; + +const ROWS = dispatchDisclosureRowsOwnedBy( + import.meta.url, + fs.readFileSync(DISPATCH_DISCLOSURE_TABLE_PATH, 'utf8'), +); + +test('every webdriver orientation dispatch-disclosure row has exactly one driver', () => { + assertDispatchDisclosureDriversMatchRows(ROWS, Object.keys(DRIVERS)); +}); + +for (const row of ROWS) { + test(`${row.id}: ${row.trigger} → dispatched ${row.dispatched}`, async () => { + const drive = DRIVERS[row.id]; + assert.ok(drive, `no driver for ${row.id}`); + await assert.rejects(drive(), (error: unknown) => { + assert.ok(error instanceof AppError); + assert.equal(error.details?.dispatched, row.dispatched); + return true; + }); + }); +} diff --git a/packages/provider-webdriver/src/webdriver-orientation.ts b/packages/provider-webdriver/src/webdriver-orientation.ts index 29e29f57db..33e7af47be 100644 --- a/packages/provider-webdriver/src/webdriver-orientation.ts +++ b/packages/provider-webdriver/src/webdriver-orientation.ts @@ -5,7 +5,7 @@ import { } from '@agent-device/contracts/device'; import { AppError } from '@agent-device/kernel/errors'; import type { WebDriverClient } from './webdriver-client.ts'; -import { isWebDriverRouteUnsupported } from './webdriver-transport.ts'; +import { isWebDriverRouteUnsupported, unsupportedRouteRefusal } from './webdriver-transport.ts'; export type WebDriverOrientationBackend = 'android' | 'xctest'; @@ -58,6 +58,7 @@ export async function setWebDriverOrientation( 'COMMAND_FAILED', `Could not set device orientation to ${rotation} on the hosted WebDriver session.`, { + ...unsupportedRouteRefusal(), hint: 'The provider driver rejected both orientation endpoints. Set the orientation as a session capability instead, for example connect --provider-device-orientation portrait.', rotation, attempts, diff --git a/packages/provider-webdriver/src/webdriver-transport.ts b/packages/provider-webdriver/src/webdriver-transport.ts index 90eb722a73..3956e7ef9d 100644 --- a/packages/provider-webdriver/src/webdriver-transport.ts +++ b/packages/provider-webdriver/src/webdriver-transport.ts @@ -334,10 +334,9 @@ function webdriverError(status: number, payload: unknown): AppError { : `WebDriver request failed with HTTP ${status}.`; if (isUnsupportedRouteAnswer(status, payload)) { return new AppError('COMMAND_FAILED', message, { - reason: WEBDRIVER_ROUTE_UNSUPPORTED_REASON, + ...unsupportedRouteRefusal(), status, response: payload, - ...dispatchDisclosure('no'), }); } return new AppError('COMMAND_FAILED', message, { @@ -352,6 +351,17 @@ function webdriverError(status: number, payload: unknown): AppError { /** Machine-readable `details.reason` of a driver answer that it does not implement the route. */ const WEBDRIVER_ROUTE_UNSUPPORTED_REASON = 'webdriver_route_unsupported'; +/** + * The details of a refusal the driver answered before running anything: it does not implement + * the route. A command that tried every route it has and was refused by each carries the same. + */ +export function unsupportedRouteRefusal(): { + reason: typeof WEBDRIVER_ROUTE_UNSUPPORTED_REASON; + dispatched: DispatchDisclosure; +} { + return { reason: WEBDRIVER_ROUTE_UNSUPPORTED_REASON, ...dispatchDisclosure('no') }; +} + /** W3C `error` codes a driver answers with when it does not implement the route. */ const UNSUPPORTED_ROUTE_ERROR_CODES: ReadonlySet = new Set([ 'unknown command',