@xenon-device-management/xenon 2.13.2 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-BwuaMjkX.js} +1 -1
- package/lib/public/assets/{BugReportButton-BoGhlI7F.js → BugReportButton-BAt0_pzq.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-BSjhk_IV.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-Cb5QZsE1.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BAaSlWbW.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-JGGYrSQQ.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-CFCp3n9d.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CbJj_EBT.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-BJeYnygR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-Bg42Atjy.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-Njz1pUIJ.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-BcTm5xV2.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-DOVdsfMQ.js} +1 -1
- package/lib/public/assets/{ai-settings-Bce5SJob.js → ai-settings-CsuJroRf.js} +2 -2
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-BKnNSTw9.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-D0SC6uw6.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-MMbTmgoE.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-BMfnGmxW.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-DuAftMWB.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-nZC2JNDO.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-hDCgTwkS.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Dda4irn_.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-BMr0Q62v.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-CZ9iw9-e.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-BRYJJVC9.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CqAJjQ5O.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DlDJtnVz.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-CpuDKysU.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-CAQydNFe.js} +1 -1
- package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-C9XG6lLv.js} +1 -1
- package/lib/public/assets/{forgot-password-DnudqFFo.js → forgot-password-CGnkRgE9.js} +1 -1
- package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BbKEXUFH.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-DJGc1M2f.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-Cm0tmSoc.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-D2DyJCml.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-UWf1GY99.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CCJv9m_I.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-f8vOVW-U.js} +1 -1
- package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-B2jDgwm7.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-ebT86qoi.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-C0aykVrd.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-BjzgWz-T.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DLG9t8FU.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-nBFSMu_X.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-CSl0UYlv.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-LeOuHqmB.js} +1 -1
- package/lib/public/assets/{runbook-page-DJTVCJX7.js → runbook-page-CMMeQml9.js} +1 -1
- package/lib/public/assets/{select-DvCSv25z.js → select-88jHcl-Z.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-B4rA1Xkv.js} +2 -2
- package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
- package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
- package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-BV98e_Sv.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BiAsnFDj.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B4-jrr-1.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-C5RSuX0e.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-BG7_rMXK.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-J-riQYyC.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-CFba5TSy.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-Cp0beJm3.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-DDsEISjV.js} +1 -1
- package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-l4VaXzq8.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +19 -21
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/openapi/control.yaml +71 -20
- package/lib/src/app/openapi/grid.yaml +23 -5
- package/lib/src/app/openapi/platform.yaml +136 -28
- package/lib/src/app/routers/control.js +12 -2
- package/lib/src/app/routers/dashboard.js +29 -1
- package/lib/src/app/routers/grid.js +32 -10
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/config.js +50 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +39 -6
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +6 -1
- package/lib/src/data-service/device-store.js +62 -2
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +150 -8
- package/lib/src/data-service/web-config-service.js +43 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +96 -40
- package/lib/src/generated/client/edge.js +17 -3
- package/lib/src/generated/client/index-browser.js +14 -0
- package/lib/src/generated/client/index.d.ts +2054 -379
- package/lib/src/generated/client/index.js +17 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +35 -7
- package/lib/src/generated/client/wasm.js +14 -0
- package/lib/src/interceptors/CommandInterceptor.js +29 -8
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- package/lib/src/plugin.js +15 -1
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AIService.js +99 -0
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +5 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/ServerManager.js +94 -8
- package/lib/src/services/SessionLifecycleService.js +47 -49
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/healing/HealingOrchestrator.js +32 -1
- package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- package/lib/src/services/recording/concurrency-gate.js +12 -3
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/scratch-database.js +1 -0
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/apple-family.spec.js +36 -0
- package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -0
- package/lib/test/unit/max-sessions.spec.js +240 -0
- package/lib/test/unit/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +1 -1
- package/lib/test/unit/partial-config-file.spec.js +14 -0
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/self-healing-boot.spec.js +75 -0
- package/lib/test/unit/self-healing-setting.spec.js +272 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/vision-assertion.spec.js +118 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +79 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +1 -1
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/schema.prisma +35 -7
- package/schema.json +19 -21
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
|
@@ -242,6 +242,33 @@ model Device {
|
|
|
242
242
|
@@index([claimSessionId])
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
/// The settings people give one phone (an admin's team, tags and maintenance,
|
|
246
|
+
/// a member's reservation), kept apart from its Device row. That row is
|
|
247
|
+
/// deleted whenever the phone goes: unplugged, rebooted, offline in adb, a
|
|
248
|
+
/// restart, its node gone. This one stays, and a new row for the phone starts
|
|
249
|
+
/// from it, in the same write (src/data-service/deviceSettings.ts). Keyed as
|
|
250
|
+
/// Device is: a phone is a udid on the server that drives it, so another
|
|
251
|
+
/// server's phone with the same udid (an emulator's) never takes these. A hub
|
|
252
|
+
/// keeps the settings of its nodes' phones here too; a node's report never
|
|
253
|
+
/// writes them.
|
|
254
|
+
model DeviceSetting {
|
|
255
|
+
udid String
|
|
256
|
+
host String
|
|
257
|
+
teamId String?
|
|
258
|
+
team Team? @relation(fields: [teamId], references: [id], onDelete: SetNull)
|
|
259
|
+
/// JSON array, as Device.tags.
|
|
260
|
+
tags String?
|
|
261
|
+
userBlocked Boolean @default(false)
|
|
262
|
+
reservationReason String?
|
|
263
|
+
reservedBy String?
|
|
264
|
+
reservedByUserId String?
|
|
265
|
+
reservedUntil Float?
|
|
266
|
+
updatedAt DateTime @updatedAt
|
|
267
|
+
|
|
268
|
+
@@id([udid, host])
|
|
269
|
+
@@index([teamId])
|
|
270
|
+
}
|
|
271
|
+
|
|
245
272
|
model PendingSession {
|
|
246
273
|
id Int @id @default(autoincrement())
|
|
247
274
|
capability_id String @unique
|
|
@@ -348,13 +375,14 @@ model ApiKey {
|
|
|
348
375
|
}
|
|
349
376
|
|
|
350
377
|
model Team {
|
|
351
|
-
id
|
|
352
|
-
name
|
|
353
|
-
createdAt
|
|
354
|
-
devices
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
378
|
+
id String @id @default(uuid())
|
|
379
|
+
name String @unique
|
|
380
|
+
createdAt DateTime @default(now())
|
|
381
|
+
devices Device[]
|
|
382
|
+
deviceSettings DeviceSetting[]
|
|
383
|
+
apiKeys ApiKey[]
|
|
384
|
+
members TeamMember[]
|
|
385
|
+
apps App[]
|
|
358
386
|
}
|
|
359
387
|
|
|
360
388
|
model SelectorState {
|
|
@@ -302,6 +302,19 @@ exports.Prisma.DeviceScalarFieldEnum = {
|
|
|
302
302
|
nodeHold: 'nodeHold'
|
|
303
303
|
};
|
|
304
304
|
|
|
305
|
+
exports.Prisma.DeviceSettingScalarFieldEnum = {
|
|
306
|
+
udid: 'udid',
|
|
307
|
+
host: 'host',
|
|
308
|
+
teamId: 'teamId',
|
|
309
|
+
tags: 'tags',
|
|
310
|
+
userBlocked: 'userBlocked',
|
|
311
|
+
reservationReason: 'reservationReason',
|
|
312
|
+
reservedBy: 'reservedBy',
|
|
313
|
+
reservedByUserId: 'reservedByUserId',
|
|
314
|
+
reservedUntil: 'reservedUntil',
|
|
315
|
+
updatedAt: 'updatedAt'
|
|
316
|
+
};
|
|
317
|
+
|
|
305
318
|
exports.Prisma.PendingSessionScalarFieldEnum = {
|
|
306
319
|
id: 'id',
|
|
307
320
|
capability_id: 'capability_id',
|
|
@@ -536,6 +549,7 @@ exports.Prisma.ModelName = {
|
|
|
536
549
|
SessionMetric: 'SessionMetric',
|
|
537
550
|
App: 'App',
|
|
538
551
|
Device: 'Device',
|
|
552
|
+
DeviceSetting: 'DeviceSetting',
|
|
539
553
|
PendingSession: 'PendingSession',
|
|
540
554
|
CLIArgs: 'CLIArgs',
|
|
541
555
|
WebhookConfig: 'WebhookConfig',
|
|
@@ -65,6 +65,8 @@ const sessionContext_1 = require("../logging/sessionContext");
|
|
|
65
65
|
const ProcessMetricsService_1 = require("../services/ProcessMetricsService");
|
|
66
66
|
const AutowaitService_1 = require("../services/autowait/AutowaitService");
|
|
67
67
|
const waitFor_1 = require("../services/autowait/waitFor");
|
|
68
|
+
const SelfHealingSwitch_1 = require("../services/settings/SelfHealingSwitch");
|
|
69
|
+
const xenonScripts_1 = require("./xenonScripts");
|
|
68
70
|
let CommandInterceptor = class CommandInterceptor {
|
|
69
71
|
constructor() {
|
|
70
72
|
this.log = logger_1.default.scope('CommandInterceptor');
|
|
@@ -182,10 +184,14 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
182
184
|
}
|
|
183
185
|
if (aiCommand === 'assertVisualState') {
|
|
184
186
|
this.log.info(`[Interceptor] Routing AI command: ${script} with payload: ${JSON.stringify(scriptArgs)}`);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
// The condition as a plain string (`execute(script, 'The cart is
|
|
188
|
+
// empty')`, which arrives as ['The cart is empty']) or as
|
|
189
|
+
// { instruction }. A plain string used to arrive as ''.
|
|
190
|
+
const first = Array.isArray(scriptArgs) ? scriptArgs[0] : scriptArgs;
|
|
191
|
+
const instruction = typeof first === 'string'
|
|
192
|
+
? first
|
|
193
|
+
: typeof first === 'object' && typeof (first === null || first === void 0 ? void 0 : first.instruction) === 'string'
|
|
194
|
+
? first.instruction
|
|
189
195
|
: '';
|
|
190
196
|
return yield typedi_1.Container.get(AICommandService_1.AICommandService).assertVisualState(driver, instruction);
|
|
191
197
|
}
|
|
@@ -216,13 +222,28 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
216
222
|
}
|
|
217
223
|
}
|
|
218
224
|
}
|
|
225
|
+
// A session-details command (`xenon: setSessionName`, ...) is answered
|
|
226
|
+
// by the before-hook; its answer is what the test gets.
|
|
227
|
+
let answered = null;
|
|
219
228
|
const shouldProceed = yield event_manager_1.DASHBORD_EVENT_MANAGER.beforeSessionCommand(sessionId, commandName, { body: { script: args[0], args: args[1] } }, {
|
|
220
|
-
status: () => ({
|
|
229
|
+
status: () => ({
|
|
230
|
+
json: (d) => {
|
|
231
|
+
var _a;
|
|
232
|
+
answered = (_a = d === null || d === void 0 ? void 0 : d.value) !== null && _a !== void 0 ? _a : null;
|
|
233
|
+
return d;
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
221
236
|
setHeader: () => { },
|
|
222
237
|
getHeader: () => { },
|
|
223
238
|
});
|
|
224
239
|
if (shouldProceed === false) {
|
|
225
|
-
return
|
|
240
|
+
return answered;
|
|
241
|
+
}
|
|
242
|
+
// Every Xenon script this server has was answered above. Any other
|
|
243
|
+
// name used to be answered with null, as if it had worked.
|
|
244
|
+
if (commandName === 'execute' && (0, xenonScripts_1.xenonScriptName)(args[0]) !== null) {
|
|
245
|
+
const { errors } = yield Promise.resolve().then(() => __importStar(require('@appium/base-driver')));
|
|
246
|
+
throw new errors.UnknownCommandError((0, xenonScripts_1.unknownXenonScriptMessage)(args[0]));
|
|
226
247
|
}
|
|
227
248
|
}
|
|
228
249
|
// --- OMNI-VISION: PROACTIVE SEARCH ---
|
|
@@ -288,7 +309,7 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
288
309
|
catch (error) {
|
|
289
310
|
if (this.isNoSuchElementError(error) &&
|
|
290
311
|
['findElement', 'findElements'].includes(commandName) &&
|
|
291
|
-
|
|
312
|
+
typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs)) {
|
|
292
313
|
// §2.7 healing-tier capability gate: a session created with
|
|
293
314
|
// xe:options.healingTiers (or the xenon:options alias) restricts
|
|
294
315
|
// self-healing to those tier indices (1=Resilio, 2=Fuzzy XML, 3=OCR,
|
|
@@ -456,7 +477,7 @@ let CommandInterceptor = class CommandInterceptor {
|
|
|
456
477
|
}, {}, JSON.stringify({ value: response, sessionId }));
|
|
457
478
|
if (commandName === 'findElement' &&
|
|
458
479
|
response &&
|
|
459
|
-
|
|
480
|
+
typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs)) {
|
|
460
481
|
this.triggerLearning(driver, args, response, sessionId);
|
|
461
482
|
}
|
|
462
483
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Xenon's execute scripts: `driver.execute('xenon: <name>', ...)`, or `xe:`.
|
|
4
|
+
*
|
|
5
|
+
* Who answers them:
|
|
6
|
+
* - The five session-details commands (SESSION_DETAILS_COMMANDS in
|
|
7
|
+
* dashboard/commands.ts) are answered by the server that keeps the
|
|
8
|
+
* session's record: a hub, for a session on a node's phone, when its
|
|
9
|
+
* dashboard is on (its gateway's before-hook), otherwise the server that
|
|
10
|
+
* drives the phone.
|
|
11
|
+
* - Every other one is answered by the server that drives the phone
|
|
12
|
+
* (CommandInterceptor), because that is where the session's autowait
|
|
13
|
+
* settings, Omni-Vision and network capture live. A hub forwards them.
|
|
14
|
+
* - A name Xenon doesn't have fails with `unknown command`.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.XENON_SCRIPT_NAMES = void 0;
|
|
18
|
+
exports.xenonScriptName = xenonScriptName;
|
|
19
|
+
exports.unknownXenonScriptMessage = unknownXenonScriptMessage;
|
|
20
|
+
/** The names CommandInterceptor and the session-details commands answer. Keep in step with both. */
|
|
21
|
+
exports.XENON_SCRIPT_NAMES = [
|
|
22
|
+
'setAutowaitProperties',
|
|
23
|
+
'getAutowaitProperties',
|
|
24
|
+
'smartTap',
|
|
25
|
+
'omniClick',
|
|
26
|
+
'visualTap',
|
|
27
|
+
'uiInventory',
|
|
28
|
+
'uiScanExport',
|
|
29
|
+
'analyzeScreen',
|
|
30
|
+
'omniScan',
|
|
31
|
+
'assertVisualState',
|
|
32
|
+
'addMock',
|
|
33
|
+
'removeMock',
|
|
34
|
+
'clearMocks',
|
|
35
|
+
'getMocks',
|
|
36
|
+
'getRequests',
|
|
37
|
+
'exportHar',
|
|
38
|
+
'setSessionName',
|
|
39
|
+
'setSessionStatus',
|
|
40
|
+
'debug',
|
|
41
|
+
'addTag',
|
|
42
|
+
'captureEvidence',
|
|
43
|
+
];
|
|
44
|
+
/** The name in a `xenon: <name>` or `xe: <name>` script, or null for any other script. */
|
|
45
|
+
function xenonScriptName(script) {
|
|
46
|
+
if (typeof script !== 'string')
|
|
47
|
+
return null;
|
|
48
|
+
const match = /^\s*(?:xenon|xe)\s*:\s*(\S+)\s*$/.exec(script);
|
|
49
|
+
return match ? match[1] : null;
|
|
50
|
+
}
|
|
51
|
+
/** The message for a `xenon:` / `xe:` script Xenon doesn't have. */
|
|
52
|
+
function unknownXenonScriptMessage(script) {
|
|
53
|
+
return (`Unknown Xenon command "${script.trim()}". ` +
|
|
54
|
+
`Xenon's execute commands are: ${exports.XENON_SCRIPT_NAMES.join(', ')}.`);
|
|
55
|
+
}
|
|
@@ -30,7 +30,7 @@ exports.DefaultPluginArgs = {
|
|
|
30
30
|
bindHostOrIp: 'auto',
|
|
31
31
|
enableDashboard: false,
|
|
32
32
|
bootedSimulators: false,
|
|
33
|
-
healthCheckIntervalMs:
|
|
33
|
+
healthCheckIntervalMs: 300000,
|
|
34
34
|
healthCheckSchedule: undefined,
|
|
35
35
|
removeDevicesFromDatabaseBeforeRunningThePlugin: false,
|
|
36
36
|
databaseProvider: undefined,
|
|
@@ -50,7 +50,6 @@ exports.DefaultPluginArgs = {
|
|
|
50
50
|
recordingCleanupMaxCount: 100,
|
|
51
51
|
recordingFailedCleanupDays: 2,
|
|
52
52
|
sessionHeartbeatIntervalMs: 30000,
|
|
53
|
-
enableJsonLogging: false,
|
|
54
53
|
autowait: {
|
|
55
54
|
enabled: false,
|
|
56
55
|
timeoutMs: 10000,
|
package/lib/src/plugin.js
CHANGED
|
@@ -67,6 +67,7 @@ const NodeDevices_1 = __importDefault(require("./device-managers/NodeDevices"));
|
|
|
67
67
|
const appiumUmbrella_1 = require("./sessions/appiumUmbrella");
|
|
68
68
|
const LiveSessionOwners_1 = require("./services/device-access/LiveSessionOwners");
|
|
69
69
|
const SessionMetricsService_1 = require("./services/metrics/SessionMetricsService");
|
|
70
|
+
const PhoneNetworkRestore_1 = require("./services/network/PhoneNetworkRestore");
|
|
70
71
|
const config_1 = require("./config");
|
|
71
72
|
const SessionManager_1 = require("./sessions/SessionManager");
|
|
72
73
|
const event_manager_1 = require("./dashboard/event-manager");
|
|
@@ -75,13 +76,22 @@ const session_service_1 = require("./dashboard/services/session-service");
|
|
|
75
76
|
const DEVICE_MANAGER_LOCK_NAME = 'DeviceManager';
|
|
76
77
|
class XenonPlugin extends base_plugin_1.default {
|
|
77
78
|
constructor(pluginName, cliArgs) {
|
|
79
|
+
var _a;
|
|
78
80
|
super(pluginName, cliArgs);
|
|
79
81
|
this.xenonLog = logger_1.default.scope('Plugin');
|
|
80
82
|
this.pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs);
|
|
81
83
|
this.aiCommandService = typedi_1.Container.get(AICommandService_1.AICommandService);
|
|
82
84
|
this.xenonLog.debug(`📱 Plugin Args: ${JSON.stringify(cliArgs)}`);
|
|
83
85
|
this.pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs, this.cliArgs);
|
|
84
|
-
|
|
86
|
+
// The option as it was given, not the merged copy above: JSON logging is on
|
|
87
|
+
// when the option says so, or when it isn't given and XENON_JSON_LOGGING does.
|
|
88
|
+
// schema.json gives the option no default, so Appium leaves it out when
|
|
89
|
+
// nobody set it; a default here (the merge above used to add `false`) would
|
|
90
|
+
// overwrite the variable on every start. `configure` ignores undefined.
|
|
91
|
+
const jsonLogging = (_a = this.cliArgs) === null || _a === void 0 ? void 0 : _a.enableJsonLogging;
|
|
92
|
+
logger_1.XenonLogger.configure({
|
|
93
|
+
enableJsonLogging: typeof jsonLogging === 'boolean' ? jsonLogging : undefined,
|
|
94
|
+
});
|
|
85
95
|
if ((0, networkAddresses_1.shouldAutoResolveBindHost)(this.pluginArgs.bindHostOrIp)) {
|
|
86
96
|
this.pluginArgs.bindHostOrIp = (0, networkAddresses_1.resolveAdvertisedBindHost)(this.pluginArgs.bindHostOrIp);
|
|
87
97
|
}
|
|
@@ -101,6 +111,10 @@ class XenonPlugin extends base_plugin_1.default {
|
|
|
101
111
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
|
|
102
112
|
if (sessionId)
|
|
103
113
|
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
114
|
+
// Appium's new-command timeout ends a session here, not in deleteSession:
|
|
115
|
+
// the phone's network (profile, interceptor proxy) and the capture are
|
|
116
|
+
// put back before the phone is released.
|
|
117
|
+
yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(sessionId, 'driver shut down');
|
|
104
118
|
const deviceFilter = {
|
|
105
119
|
session_id: sessionId ? sessionId : undefined,
|
|
106
120
|
udid: driver.caps && driver.caps.udid ? driver.caps.udid : undefined,
|
package/lib/src/prisma.js
CHANGED
|
@@ -33,7 +33,7 @@ exports.getPrismaClient = getPrismaClient;
|
|
|
33
33
|
* can stub individual methods (Prisma's internal Proxy blocks property assignment).
|
|
34
34
|
*/
|
|
35
35
|
const MODEL_DELEGATES = new Set([
|
|
36
|
-
'build', 'session', 'sessionLog', 'log', 'profiling', 'app', 'device',
|
|
36
|
+
'build', 'session', 'sessionLog', 'log', 'profiling', 'app', 'device', 'deviceSetting',
|
|
37
37
|
'pendingSession', 'cLIArgs', 'webhookConfig', 'webConfig', 'locatorEtalon',
|
|
38
38
|
'lease', 'portLease', 'apiKey', 'selectorState', 'user', 'userSession',
|
|
39
39
|
'passwordResetToken', 'team', 'teamMember', 'eventLog', 'project', 'recording', 'bookmark',
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.clientProviderFromSchema = clientProviderFromSchema;
|
|
40
|
+
exports.describeDatabaseUrl = describeDatabaseUrl;
|
|
41
|
+
exports.checkDatabase = checkDatabase;
|
|
42
|
+
exports.assertSupportedDatabase = assertSupportedDatabase;
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
46
|
+
const config_1 = require("../config");
|
|
47
|
+
const LABEL = { sqlite: 'SQLite', postgresql: 'PostgreSQL' };
|
|
48
|
+
function providerOfUrl(url) {
|
|
49
|
+
if (url.startsWith('file:'))
|
|
50
|
+
return 'sqlite';
|
|
51
|
+
if (/^postgres(ql)?:\/\//.test(url))
|
|
52
|
+
return 'postgresql';
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/** The datasource provider in a Prisma schema's text, or null if it has none. */
|
|
56
|
+
function clientProviderFromSchema(schema) {
|
|
57
|
+
const match = /datasource\s+\w+\s*{[^}]*?provider\s*=\s*"([^"]+)"/.exec(schema);
|
|
58
|
+
return match ? match[1] : null;
|
|
59
|
+
}
|
|
60
|
+
/** A database URL fit for a log line: a SQLite file's path, or a server URL without credentials. */
|
|
61
|
+
function describeDatabaseUrl(url) {
|
|
62
|
+
if (url.startsWith('file:'))
|
|
63
|
+
return url.slice('file:'.length);
|
|
64
|
+
try {
|
|
65
|
+
const parsed = new URL(url);
|
|
66
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
67
|
+
}
|
|
68
|
+
catch (_a) {
|
|
69
|
+
return url.split(':')[0] + ':';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Decide whether the server may start on `url`. Throws an Error whose message
|
|
74
|
+
* says what to change; returns a warning when `configured` disagrees with the
|
|
75
|
+
* client but the URL is usable.
|
|
76
|
+
*/
|
|
77
|
+
function checkDatabase(opts) {
|
|
78
|
+
var _a;
|
|
79
|
+
const { configured, url, client } = opts;
|
|
80
|
+
const clientLabel = (_a = LABEL[client]) !== null && _a !== void 0 ? _a : client;
|
|
81
|
+
const urlProvider = providerOfUrl(url);
|
|
82
|
+
const where = describeDatabaseUrl(url);
|
|
83
|
+
if (urlProvider !== client) {
|
|
84
|
+
const kind = urlProvider
|
|
85
|
+
? `a ${LABEL[urlProvider]} database`
|
|
86
|
+
: 'not a database this install knows';
|
|
87
|
+
const fix = client === 'sqlite'
|
|
88
|
+
? 'Set DATABASE_URL (or databaseUrl) to a SQLite file, `file:/path/to/xenon.db`, or leave it ' +
|
|
89
|
+
'unset to use the default under ~/.cache/xenon. The published plugin stores its data in SQLite only.'
|
|
90
|
+
: `Set DATABASE_URL (or databaseUrl) to a ${clientLabel} URL.`;
|
|
91
|
+
throw new Error(`[Database] Cannot start: the database URL (${where}) is ${kind}, but this install's ` +
|
|
92
|
+
`database client is built for ${clientLabel}. ${fix}`);
|
|
93
|
+
}
|
|
94
|
+
if (configured && configured !== client) {
|
|
95
|
+
return {
|
|
96
|
+
warning: `[Database] databaseProvider is "${configured}", but this install's database client is ` +
|
|
97
|
+
`built for ${clientLabel}, so the setting has no effect. Your data is in ${where}. ` +
|
|
98
|
+
`Set databaseProvider to "${client}", or remove it.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
/** The provider the generated client next to this file was built for. */
|
|
104
|
+
function generatedClientProvider() {
|
|
105
|
+
var _a;
|
|
106
|
+
const schemaPath = path.join(__dirname, '..', 'generated', 'client', 'schema.prisma');
|
|
107
|
+
try {
|
|
108
|
+
return (_a = clientProviderFromSchema(fs.readFileSync(schemaPath, 'utf8'))) !== null && _a !== void 0 ? _a : 'sqlite';
|
|
109
|
+
}
|
|
110
|
+
catch (_b) {
|
|
111
|
+
// The published plugin's client is SQLite; a missing copy means a plain install.
|
|
112
|
+
return 'sqlite';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Startup gate: log the warning, or throw so the server doesn't start on the wrong database. */
|
|
116
|
+
function assertSupportedDatabase() {
|
|
117
|
+
const { warning } = checkDatabase({
|
|
118
|
+
configured: config_1.config.databaseProvider,
|
|
119
|
+
url: config_1.config.databaseUrl,
|
|
120
|
+
client: generatedClientProvider(),
|
|
121
|
+
});
|
|
122
|
+
if (warning)
|
|
123
|
+
logger_1.default.warn(warning);
|
|
124
|
+
}
|
|
@@ -46,6 +46,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
48
|
exports.AI_SERVICE = exports.AIService = void 0;
|
|
49
|
+
exports.parseVisualVerdict = parseVisualVerdict;
|
|
49
50
|
const generative_ai_1 = require("@google/generative-ai");
|
|
50
51
|
const openai_1 = __importDefault(require("openai"));
|
|
51
52
|
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
@@ -55,6 +56,41 @@ const fs = __importStar(require("fs"));
|
|
|
55
56
|
const path = __importStar(require("path"));
|
|
56
57
|
const config_1 = require("../config");
|
|
57
58
|
const CircuitBreaker_1 = require("./CircuitBreaker");
|
|
59
|
+
/** What GeminiProvider answers, instead of throwing, when the provider is rate-limited (429). */
|
|
60
|
+
const RATE_LIMITED = 'CONNECTION_OK_RATE_LIMITED';
|
|
61
|
+
/**
|
|
62
|
+
* A provider's answer to the assertion prompt, `{ "result": true|false,
|
|
63
|
+
* "reason": "..." }`, read leniently (code fences, text around the JSON,
|
|
64
|
+
* `message` for `reason`, "true"/"false" as strings) but never guessed: null
|
|
65
|
+
* when it holds no true/false `result`.
|
|
66
|
+
*/
|
|
67
|
+
function parseVisualVerdict(answer) {
|
|
68
|
+
const text = answer.replace(/```(?:json)?/gi, '').trim();
|
|
69
|
+
const start = text.indexOf('{');
|
|
70
|
+
const end = text.lastIndexOf('}');
|
|
71
|
+
if (start === -1 || end <= start)
|
|
72
|
+
return null;
|
|
73
|
+
let data;
|
|
74
|
+
try {
|
|
75
|
+
data = JSON.parse(text.slice(start, end + 1));
|
|
76
|
+
}
|
|
77
|
+
catch (_a) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const raw = data === null || data === void 0 ? void 0 : data.result;
|
|
81
|
+
const result = raw === true || raw === 'true' ? true : raw === false || raw === 'false' ? false : null;
|
|
82
|
+
if (result === null)
|
|
83
|
+
return null;
|
|
84
|
+
const reason = typeof data.reason === 'string'
|
|
85
|
+
? data.reason
|
|
86
|
+
: typeof data.message === 'string'
|
|
87
|
+
? data.message
|
|
88
|
+
: '';
|
|
89
|
+
return {
|
|
90
|
+
result,
|
|
91
|
+
message: reason.trim() || (result ? 'The condition holds.' : 'The condition does not hold.'),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
58
94
|
class GeminiProvider {
|
|
59
95
|
constructor(apiKey, modelName) {
|
|
60
96
|
this.genAI = new generative_ai_1.GoogleGenerativeAI(apiKey.trim());
|
|
@@ -365,6 +401,69 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
|
|
|
365
401
|
}
|
|
366
402
|
});
|
|
367
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Asks the provider whether `condition` holds on the screenshot
|
|
406
|
+
* (`xenon: assertVisualState`). Answers only with the provider's own
|
|
407
|
+
* verdict: with no provider, a failed call, a rate limit or an answer that
|
|
408
|
+
* isn't true or false it throws, saying the condition was not checked. A
|
|
409
|
+
* `false` there would pass a test asserting that something is absent.
|
|
410
|
+
*/
|
|
411
|
+
assertVisual(screenshotBase64, condition) {
|
|
412
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
413
|
+
var _a;
|
|
414
|
+
this.initializeProvider();
|
|
415
|
+
const notChecked = 'so the condition was not checked';
|
|
416
|
+
if (!this.provider) {
|
|
417
|
+
throw new Error(`No AI provider is configured, ${notChecked}. Set XENON_AI_PROVIDER and the provider's key.`);
|
|
418
|
+
}
|
|
419
|
+
const prompt = `
|
|
420
|
+
You are checking a screenshot of a mobile app for an automated test.
|
|
421
|
+
|
|
422
|
+
Condition: ${JSON.stringify(condition)}
|
|
423
|
+
|
|
424
|
+
Is the condition true of what the screenshot shows? Judge only what is visible on the screen.
|
|
425
|
+
Answer with JSON only, exactly: {"result": true or false, "reason": "one sentence on what you see"}
|
|
426
|
+
`.trim();
|
|
427
|
+
let answer;
|
|
428
|
+
try {
|
|
429
|
+
answer = yield this.callProvider(prompt, screenshotBase64);
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
throw new Error(`The AI provider failed, ${notChecked}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
433
|
+
}
|
|
434
|
+
if (answer === RATE_LIMITED) {
|
|
435
|
+
throw new Error(`The AI provider is rate-limited, ${notChecked}. Try again later.`);
|
|
436
|
+
}
|
|
437
|
+
const verdict = parseVisualVerdict(answer);
|
|
438
|
+
if (!verdict) {
|
|
439
|
+
const said = answer.replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
440
|
+
throw new Error(`Xenon could not read the AI provider's answer as true or false, ${notChecked}. ` +
|
|
441
|
+
`It answered: ${JSON.stringify(said)}`);
|
|
442
|
+
}
|
|
443
|
+
return verdict;
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* A short description of the screen in the screenshot, for
|
|
448
|
+
* `xenon: analyzeScreen` and device control's Omni-Scan. Throws when there
|
|
449
|
+
* is no provider or the call fails, so the caller can say why there is none.
|
|
450
|
+
*/
|
|
451
|
+
describeScreen(screenshotBase64) {
|
|
452
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
453
|
+
this.initializeProvider();
|
|
454
|
+
if (!this.provider)
|
|
455
|
+
throw new Error('No AI provider is configured');
|
|
456
|
+
const prompt = `
|
|
457
|
+
You are looking at a screenshot of a mobile app, for a software tester.
|
|
458
|
+
Describe the screen in a few short sentences: which screen it is, its main elements and their state (for example filled in, empty, selected or disabled), and anything unexpected, such as an error message, a system dialog, a crash or a loading indicator.
|
|
459
|
+
Describe only what is visible. Answer in plain text.
|
|
460
|
+
`.trim();
|
|
461
|
+
const answer = yield this.callProvider(prompt, screenshotBase64);
|
|
462
|
+
if (answer === RATE_LIMITED)
|
|
463
|
+
throw new Error('The AI provider is rate-limited');
|
|
464
|
+
return answer;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
368
467
|
/**
|
|
369
468
|
* Heals a broken locator using deep LLM reasoning (Tier 5)
|
|
370
469
|
*/
|
|
@@ -25,6 +25,7 @@ const logger_1 = __importDefault(require("../logger"));
|
|
|
25
25
|
const fs_1 = __importDefault(require("fs"));
|
|
26
26
|
const path_1 = __importDefault(require("path"));
|
|
27
27
|
const config_1 = require("../config");
|
|
28
|
+
const labSettings_1 = require("./settings/labSettings");
|
|
28
29
|
const recordingCleanup_1 = require("./recording/recordingCleanup");
|
|
29
30
|
/** Subdirectory holding composite (multi-device) outputs, keyed by group id. */
|
|
30
31
|
const GROUPS_DIR = '_groups';
|
|
@@ -34,6 +35,8 @@ let CleanupService = class CleanupService {
|
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
36
37
|
* Orchestrates build + session + recording cleanup based on retention policy.
|
|
38
|
+
* The build retention window, the build cap and the asset purge are the ones
|
|
39
|
+
* saved on the dashboard's Maintenance page, else `pluginArgs`'.
|
|
37
40
|
*
|
|
38
41
|
* Phases:
|
|
39
42
|
* 1. Purge old builds (by age AND count cap) together with their sessions.
|
|
@@ -44,7 +47,9 @@ let CleanupService = class CleanupService {
|
|
|
44
47
|
*/
|
|
45
48
|
runCleanup(pluginArgs) {
|
|
46
49
|
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
-
|
|
50
|
+
// What the Maintenance page saved wins over the startup options, and is
|
|
51
|
+
// read here, at each run, so a change needs no restart. See labSettings.ts.
|
|
52
|
+
const { buildCleanupDays, buildCleanupMaxCount, deleteBuildAssets } = yield (0, labSettings_1.loadEffectiveSettings)(pluginArgs);
|
|
48
53
|
this.log.info(`Starting cleanup: Retention = ${buildCleanupDays} days, Max Builds = ${buildCleanupMaxCount}, Purge Assets = ${deleteBuildAssets}`);
|
|
49
54
|
const expirationDate = new Date();
|
|
50
55
|
expirationDate.setDate(expirationDate.getDate() - buildCleanupDays);
|