@xenon-device-management/xenon 2.12.0 → 2.13.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/lib/package.json +2 -2
- package/lib/public/assets/{AnnotationOverlay-Crkn72_3.js → AnnotationOverlay-CzbFYtPI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BThVlj_j.js → ApiKeyGate-CA5KFHHJ.js} +1 -1
- package/lib/public/assets/{BugReportButton-Dwa0GnBh.js → BugReportButton-D-DSGg8C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-Ccc-pOls.js → DeviceMosaicView-paDEFU4B.js} +1 -1
- package/lib/public/assets/{EmptyState-BeuTrlN_.js → EmptyState-BE_5f8Vw.js} +1 -1
- package/lib/public/assets/{FieldGroup-DBIaNxl1.js → FieldGroup-DqMf7Rf3.js} +1 -1
- package/lib/public/assets/{FilterMenu-BgSAS6iT.js → FilterMenu-BQY75Su3.js} +1 -1
- package/lib/public/assets/{Menu-GCbP6gYN.js → Menu-BiW3os9u.js} +1 -1
- package/lib/public/assets/{Modal-ByESeBRC.js → Modal-DfawTxih.js} +1 -1
- package/lib/public/assets/{RecordingPage-Bwyf3WMl.js → RecordingPage-BvemyRiy.js} +1 -1
- package/lib/public/assets/{RecordingsPage-7kM1rB3i.js → RecordingsPage-CXfw7wpd.js} +1 -1
- package/lib/public/assets/{SegmentedControl-B8tTU8kD.js → SegmentedControl-B7m9kpom.js} +1 -1
- package/lib/public/assets/{SettingCard-Clg_Ijbc.js → SettingCard-Cw6lDD0l.js} +1 -1
- package/lib/public/assets/{Table-CIaR0QWV.js → Table-DlEuFgv-.js} +1 -1
- package/lib/public/assets/{activity-Bengh9SD.js → activity-CnLvNwae.js} +1 -1
- package/lib/public/assets/{ai-settings-G9SymiPH.js → ai-settings-D7Te8o-a.js} +1 -1
- package/lib/public/assets/{api-keys-CrGlbfn-.js → api-keys-DphSVzBr.js} +1 -1
- package/lib/public/assets/{apps-DIbcGVlU.js → apps-vUfu15lX.js} +1 -1
- package/lib/public/assets/{arrow-left-BtJIXXMY.js → arrow-left-CLQWAiCY.js} +1 -1
- package/lib/public/assets/{arrow-right-zm-NagQh.js → arrow-right-K6A5e_Ee.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DdU8YEYp.js → arrow-up-right-DUA_lq6f.js} +1 -1
- package/lib/public/assets/{auth-shell-CHmBFmOz.js → auth-shell-BYRHcZd6.js} +2 -2
- package/lib/public/assets/{builds-page-JkzWKRpU.js → builds-page-C0fTvIG8.js} +1 -1
- package/lib/public/assets/{button-CqhPBGRj.js → button-BxnHCCsX.js} +1 -1
- package/lib/public/assets/{calendar-C7eYYJTo.js → calendar-CqVsn28A.js} +1 -1
- package/lib/public/assets/{check-DpbIM4E0.js → check-R6V3bot0.js} +1 -1
- package/lib/public/assets/{chevron-right-Dl4X1PRz.js → chevron-right-BA2Fntu-.js} +1 -1
- package/lib/public/assets/{circle-check-Du3sjbfV.js → circle-check-D4EzyGzs.js} +1 -1
- package/lib/public/assets/{circle-x-BSSqhpXi.js → circle-x-Cf6AF2B2.js} +1 -1
- package/lib/public/assets/{clock-DMT60v1C.js → clock-EZfet0sS.js} +1 -1
- package/lib/public/assets/{copy-BKNyOehd.js → copy-Bc6P8v9V.js} +1 -1
- package/lib/public/assets/{device-explorer-CoPR8DW3.js → device-explorer-BFXpRq6d.js} +1 -1
- package/lib/public/assets/{download-BM6Xn22t.js → download-Bd8OGLzi.js} +1 -1
- package/lib/public/assets/{forgot-password-B8WqMqBT.js → forgot-password-CoE7Ugtx.js} +1 -1
- package/lib/public/assets/{index-DauQh6ie.js → index-BiL0Enl_.js} +1 -1
- package/lib/public/assets/{index-ClrpAMAT.js → index-CHOr4JCs.js} +2 -2
- package/lib/public/assets/{input-CyKdLnEx.js → input-CJ8z3IO5.js} +1 -1
- package/lib/public/assets/{line-chart-EIBXwYGo.js → line-chart-CPK0ObjV.js} +1 -1
- package/lib/public/assets/{list-checks-fWsgD9bI.js → list-checks-mXGHUVbB.js} +1 -1
- package/lib/public/assets/{lock-CVCe56TH.js → lock-BSXM1xvq.js} +1 -1
- package/lib/public/assets/{login-BJ8a7yVD.js → login-Ct2iW066.js} +1 -1
- package/lib/public/assets/{maintenance-settings-DcRmTLS6.js → maintenance-settings-DaOs00P6.js} +1 -1
- package/lib/public/assets/{monitor-Bw1YQZnL.js → monitor-D8WQK8md.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-C01jbqkO.js → mouse-pointer-2-Cw2tWzDX.js} +1 -1
- package/lib/public/assets/{network-CBGUjJDJ.js → network-Ba_uFxWo.js} +1 -1
- package/lib/public/assets/{overview-Cxe8aQ7C.js → overview-CxrZ7_6k.js} +1 -1
- package/lib/public/assets/{page-header-B92DKLiq.js → page-header-Cm1XL49f.js} +1 -1
- package/lib/public/assets/{play-Ck0L-0_m.js → play-C0CuKLXj.js} +1 -1
- package/lib/public/assets/{plus-Dq3tCy2N.js → plus-CIIvR_6Z.js} +1 -1
- package/lib/public/assets/{profile-page--dkDiKbr.js → profile-page-BZ_e-Bgw.js} +1 -1
- package/lib/public/assets/{recording-group-store-5BYIFFN9.js → recording-group-store-BWeeNs2_.js} +1 -1
- package/lib/public/assets/{reset-password-KYWlfjia.js → reset-password-lfNz8Rm1.js} +1 -1
- package/lib/public/assets/{runbook-page-mnsxgS1d.js → runbook-page-Cnec3VSx.js} +1 -1
- package/lib/public/assets/{select-BBOIZTYm.js → select-CZuCnold.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-CcI2j_Su.js → selector-detail-redirect-BG3pxFLw.js} +1 -1
- package/lib/public/assets/{selector-health-page-BdJb_x5L.js → selector-health-page-DsyL58tb.js} +1 -1
- package/lib/public/assets/{session-detail-page-BjLaJy8z.js → session-detail-page-ZZyGM413.js} +1 -1
- package/lib/public/assets/{settings-B6cpMhsx.js → settings-DGZ7B_AR.js} +1 -1
- package/lib/public/assets/{stat-tile-BU9e4s36.js → stat-tile-DFhv8JoI.js} +1 -1
- package/lib/public/assets/{tablet-hgbEwrWq.js → tablet-68J5f6zh.js} +1 -1
- package/lib/public/assets/{teams-uiiG4hZM.js → teams-eHSyEpbD.js} +1 -1
- package/lib/public/assets/{trash-2-NK_Iazmg.js → trash-2-CcSsv4fS.js} +1 -1
- package/lib/public/assets/{upload-BUX8TNFi.js → upload-BYf26K71.js} +1 -1
- package/lib/public/assets/{use-builds-data-DZszmFyl.js → use-builds-data-CHW-MhaP.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-BK4B9kRC.js → use-password-reset-mode-DozShXkk.js} +1 -1
- package/lib/public/assets/{users-BU4XBbMT.js → users-BpYnpOl2.js} +1 -1
- package/lib/public/assets/{users-BDh1xjad.js → users-CcSQRoK9.js} +1 -1
- package/lib/public/assets/{video-off-BOjNwT4R.js → video-off-dFKgps73.js} +1 -1
- package/lib/public/assets/{webhook-settings-BMzyUcx5.js → webhook-settings-DD1XjiOD.js} +1 -1
- package/lib/public/assets/{zap-DTWwVMUg.js → zap-COC2tZaH.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/apiErrors.js +118 -0
- package/lib/src/app/index.js +6 -1
- package/lib/src/app/openapi/control.yaml +3129 -0
- package/lib/src/app/openapi/grid.yaml +2295 -0
- package/lib/src/app/openapi/identity.yaml +2168 -0
- package/lib/src/app/openapi/platform.yaml +2885 -0
- package/lib/src/app/openapi/sessions.yaml +3784 -0
- package/lib/src/app/routers/bug-report.js +4 -1
- package/lib/src/app/routers/config.js +6 -107
- package/lib/src/app/routers/control.js +117 -59
- package/lib/src/app/routers/dashboard.js +13 -7
- package/lib/src/app/routers/grid.js +54 -14
- package/lib/src/app/routers/profile.js +27 -13
- package/lib/src/app/routers/recordings.js +11 -5
- package/lib/src/app/routers/reservation.js +63 -15
- package/lib/src/app/routers/users.js +4 -0
- package/lib/src/app/routers/webhook.js +17 -8
- package/lib/src/app/swagger.js +259 -177
- package/lib/src/data-service/device-service.js +4 -1
- package/lib/src/data-service/deviceFieldOwners.js +1 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +5 -2
- package/lib/src/device-managers/ios/WDAClient.js +32 -32
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +38 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +2 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/middleware/csrfMiddleware.js +13 -5
- package/lib/src/middleware/rateLimitMiddleware.js +19 -5
- package/lib/src/middleware/roleGuard.js +20 -0
- package/lib/src/services/AIService.js +13 -3
- package/lib/src/services/NotificationService.js +28 -28
- package/lib/src/services/bug-report/BugReportService.js +8 -2
- package/lib/src/services/lease/LeaseService.js +71 -20
- package/lib/src/services/omni-vision/OmniVisionService.js +14 -5
- package/lib/src/services/recording/RecordingOrchestrator.js +16 -2
- package/lib/test/helpers/expressRoutes.js +41 -0
- package/lib/test/integration/team-visibility-control.spec.js +2 -2
- package/lib/test/unit/access-scopes.spec.js +227 -0
- package/lib/test/unit/api-error-handling.spec.js +179 -0
- package/lib/test/unit/bug-report/route.spec.js +29 -0
- package/lib/test/unit/bug-report/service.spec.js +30 -0
- package/lib/test/unit/control-honest-answers.spec.js +134 -0
- package/lib/test/unit/device-allocation-routes.spec.js +232 -0
- package/lib/test/unit/healing-state-endpoints.spec.js +8 -5
- package/lib/test/unit/install-repository-app-team.spec.js +4 -1
- package/lib/test/unit/lease/LeaseService.spec.js +5 -4
- package/lib/test/unit/lease/lease-device-match.spec.js +161 -0
- package/lib/test/unit/lease/lease-session-ownership.spec.js +10 -9
- package/lib/test/unit/omni-vision-failures.spec.js +81 -0
- package/lib/test/unit/openapi-coverage.spec.js +114 -0
- package/lib/test/unit/profile-router.test.js +42 -0
- package/lib/test/unit/rateLimitMiddleware.test.js +49 -0
- package/lib/test/unit/recording-orchestrator.spec.js +80 -0
- package/lib/test/unit/recordings-library-routes.spec.js +37 -0
- package/lib/test/unit/reservation-team-visibility.spec.js +4 -3
- package/lib/test/unit/reset-link.test.js +2 -1
- package/lib/test/unit/stream-ticket-identity.spec.js +1 -1
- package/lib/test/unit/users-router.test.js +14 -1
- package/lib/test/unit/wda-client-failures.spec.js +90 -0
- package/lib/test/unit/webhook-delivery.spec.js +142 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/prisma/migrations/20261004120000_reservation_holder/migration.sql +2 -0
- package/prisma/schema.prisma +2 -0
- package/lib/src/app/swagger-docs.js +0 -1701
package/lib/src/app/swagger.js
CHANGED
|
@@ -7,97 +7,220 @@ exports.swaggerSpec = void 0;
|
|
|
7
7
|
exports.setupSwagger = setupSwagger;
|
|
8
8
|
const swagger_jsdoc_1 = __importDefault(require("swagger-jsdoc"));
|
|
9
9
|
const swagger_ui_express_1 = __importDefault(require("swagger-ui-express"));
|
|
10
|
+
const package_json_1 = __importDefault(require("../../package.json"));
|
|
11
|
+
/**
|
|
12
|
+
* The API reference served at /xenon/api-docs (and as JSON at
|
|
13
|
+
* /xenon/api-docs.json). The shared parts live here: the introduction,
|
|
14
|
+
* authentication, the error and rate-limit responses, and the tags. Each
|
|
15
|
+
* area's paths live in src/app/openapi/<area>.yaml.
|
|
16
|
+
*
|
|
17
|
+
* test/unit/openapi-coverage.spec.ts keeps it whole: every route the server
|
|
18
|
+
* serves must be documented, every documented route must be served, and the
|
|
19
|
+
* result must be valid OpenAPI 3.
|
|
20
|
+
*/
|
|
21
|
+
const errorExample = (error, message) => ({
|
|
22
|
+
'application/json': {
|
|
23
|
+
schema: { $ref: '#/components/schemas/Error' },
|
|
24
|
+
example: message ? { error, message } : { error },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
10
27
|
const swaggerDefinition = {
|
|
11
|
-
openapi: '3.0.
|
|
12
|
-
servers: [
|
|
13
|
-
{
|
|
14
|
-
url: '/xenon',
|
|
15
|
-
description: 'Local Grid Node Registry',
|
|
16
|
-
},
|
|
17
|
-
],
|
|
28
|
+
openapi: '3.0.3',
|
|
29
|
+
servers: [{ url: '/xenon', description: 'This Xenon server' }],
|
|
18
30
|
info: {
|
|
19
31
|
title: 'Xenon API',
|
|
20
|
-
version:
|
|
32
|
+
version: package_json_1.default.version,
|
|
21
33
|
description: `
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
The REST API of a Xenon server: the devices in your lab, the Appium sessions that run on them, live device control, recordings, selector health and administration. The dashboard uses this same API.
|
|
35
|
+
|
|
36
|
+
This reference describes version **${package_json_1.default.version}**. Every path below is relative to \`/xenon\`, so \`/api/devices\` is \`https://<your-xenon-host>/xenon/api/devices\`. The raw OpenAPI document is at [\`/xenon/api-docs.json\`](/xenon/api-docs.json).
|
|
37
|
+
|
|
38
|
+
## Authentication
|
|
39
|
+
|
|
40
|
+
Every endpoint needs a credential, except the few marked as public (health checks, sign-in, password reset and the JWKS). The four ways to authenticate are:
|
|
41
|
+
|
|
42
|
+
| Credential | How to send it | Typical use |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| **Access key and API token** | \`x-xenon-access-key\` and \`x-xenon-token\` headers | Scripts, CI, SDKs, a node reporting to its hub |
|
|
45
|
+
| **Bearer token** | \`Authorization: Bearer <jwt>\` | Short-lived access; mint one with \`POST /api/auth/token\` (audience \`xenon-rest\`, 1 hour) |
|
|
46
|
+
| **Dashboard session** | \`xenon_dashboard_session\` cookie, set by \`POST /api/auth/login\` | The browser dashboard |
|
|
47
|
+
| **Hub token** | \`x-xenon-hub-token\` header | Hub to node only; Xenon sends it itself |
|
|
48
|
+
|
|
49
|
+
Find your access key and create API tokens on your profile page (\`/xenon/profile\`, **API tokens**).
|
|
50
|
+
|
|
51
|
+
**Scopes.** An API token carries scopes: \`read\`, \`sessions\`, \`devices\` and \`admin\`. \`admin\` satisfies any scope. A signed-in dashboard user gets the scopes of their role: members have \`devices\`, \`sessions\` and \`read\`; admins also have \`admin\`. A request without the scope it needs gets \`403\`.
|
|
52
|
+
|
|
53
|
+
**Roles.** Some endpoints also need a role: \`MEMBER\`, \`ADMIN\` or \`SUPER_ADMIN\`. Each says so in its description.
|
|
54
|
+
|
|
55
|
+
**Teams.** A member sees only their teams' devices and the shared pool, and the sessions, apps and selectors that go with them. Something outside your teams answers exactly as if it didn't exist: \`404\`.
|
|
56
|
+
|
|
57
|
+
**Same-origin check.** A state-changing request (POST, PUT, PATCH, DELETE) must carry an \`Origin\` or \`Referer\` header from the same host as the server, or it gets \`403\`. Requests sent with the access key and token headers, an \`Authorization: Bearer\` token, or a hub token are exempt: a browser can't send those headers from another site. So only a script that calls a public endpoint such as \`POST /api/auth/login\` needs to send an \`Origin\` header.
|
|
58
|
+
|
|
59
|
+
## Errors
|
|
60
|
+
|
|
61
|
+
Errors are JSON with an \`error\` field. An unexpected server error is \`500\` with \`{ "error": "internal", "message": "Internal server error" }\`; the details go to the server's log. Newer endpoints put a stable, machine-readable code there (\`not_found\`, \`device_held_by_another_user\`) with a human-readable \`message\`. Older ones put the message itself in \`error\`, or \`true\` with a \`message\`. Branch on the HTTP status first.
|
|
62
|
+
|
|
63
|
+
## Rate limits
|
|
24
64
|
|
|
25
|
-
|
|
65
|
+
Every request with a credential is rate limited, except single-use tickets. An access key has its own budgets; a signed-in user's dashboard sessions and bearer tokens share one set of budgets per user (300 a minute). Each has three request budgets, refilled every minute:
|
|
66
|
+
- \`read\`: GET requests (ten times the budget for a signed-in user or a bearer token, since a dashboard page loads many images at once);
|
|
67
|
+
- \`heavy\`: AI, healing and visual endpoints (a quarter of the budget, at least 10);
|
|
68
|
+
- \`control\`: everything else.
|
|
26
69
|
|
|
27
|
-
|
|
28
|
-
* **Elastic Device Grid**: Real-time discovery and state management of physical nodes.
|
|
29
|
-
* **Autonomous Sessions**: Self-healing Appium session management with automated retries.
|
|
30
|
-
* **Deep Control**: Direct low-latency interaction logic (Input injection, thermal monitoring).
|
|
31
|
-
* **Security Protocol**: Industrial-grade isolation for sensitive test environments.
|
|
70
|
+
Each such response carries \`X-RateLimit-Category\`, \`X-RateLimit-Remaining\` and \`X-RateLimit-Capacity\`. Past the budget the answer is \`429\` with \`Retry-After\` in seconds.
|
|
32
71
|
|
|
33
|
-
|
|
34
|
-
### Authentication
|
|
35
|
-
Every endpoint authenticates with the per-user \`(x-xenon-access-key, x-xenon-token)\` pair. Each token carries one or more scopes — \`read\`, \`sessions\`, \`devices\`, or \`admin\` (admin always satisfies any scope check). Mint tokens at \`/profile\` → **API Tokens**; the access key is shown at the top of that page and is rotatable. Browser dashboards may use the \`xenon_dashboard_session\` cookie set by \`POST /api/auth/login\`.
|
|
72
|
+
## Appium sessions
|
|
36
73
|
|
|
37
|
-
|
|
38
|
-
|
|
74
|
+
Appium's WebDriver API (\`POST /session\` and the session's commands, under Appium's own base path) is not part of this reference. A session authenticates in its capabilities, under \`xe:options\`, with one of:
|
|
75
|
+
- \`accessKey\` and \`token\`;
|
|
76
|
+
- \`sessionToken\`, minted by \`POST /api/auth/token\`.
|
|
39
77
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
78
|
+
A leased device also takes \`leaseId\` and \`leaseToken\`, which the lease's \`appiumCapabilities\` already carry. Xenon removes these credentials before the driver or any stored record sees them.
|
|
79
|
+
|
|
80
|
+
## Live streams
|
|
81
|
+
|
|
82
|
+
Device previews and live logs use WebSockets, outside this reference:
|
|
83
|
+
- \`/xenon/api/control/{udid}/stream/h264\`
|
|
84
|
+
- \`/xenon/api/control/{udid}/logcat\`
|
|
85
|
+
|
|
86
|
+
Both are opened with a ticket from \`POST /api/control/{udid}/stream/ticket\`. Socket.IO at \`/socket.io/\` carries the dashboard's live events.
|
|
87
|
+
`,
|
|
43
88
|
contact: {
|
|
44
|
-
name: 'Xenon
|
|
45
|
-
url: 'https://github.com/
|
|
89
|
+
name: 'Xenon',
|
|
90
|
+
url: 'https://github.com/Rabindra184/xenon',
|
|
46
91
|
},
|
|
47
92
|
license: {
|
|
48
|
-
name: 'ISC
|
|
93
|
+
name: 'ISC',
|
|
49
94
|
url: 'https://opensource.org/licenses/ISC',
|
|
50
95
|
},
|
|
51
96
|
},
|
|
52
97
|
tags: [
|
|
53
|
-
{ name: 'Health & Ops', description: 'Liveness, version, metrics
|
|
54
|
-
{ name: 'Authentication', description: '
|
|
55
|
-
{ name: '
|
|
56
|
-
{ name: '
|
|
57
|
-
{ name: '
|
|
58
|
-
{ name: '
|
|
59
|
-
{ name: '
|
|
60
|
-
{ name: '
|
|
61
|
-
{ name: '
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{ name: '
|
|
67
|
-
{
|
|
98
|
+
{ name: 'Health & Ops', description: 'Liveness, version, metrics and server information.' },
|
|
99
|
+
{ name: 'Authentication', description: 'Sign-in, sign-out, password reset and tokens.' },
|
|
100
|
+
{ name: 'Profile', description: 'Your own account: API tokens and access key.' },
|
|
101
|
+
{ name: 'Users', description: 'User accounts. Admin only.' },
|
|
102
|
+
{ name: 'Teams', description: 'Teams, their members and the devices they own.' },
|
|
103
|
+
{ name: 'API Keys', description: 'Every API key in the lab. Admin only.' },
|
|
104
|
+
{ name: 'Projects', description: 'Projects that group work in the lab.' },
|
|
105
|
+
{ name: 'Audit', description: 'Audit events sent in by Xenon services.' },
|
|
106
|
+
{ name: 'Devices', description: 'The devices in the lab, and blocking them for maintenance.' },
|
|
107
|
+
{
|
|
108
|
+
name: 'Control',
|
|
109
|
+
description: 'Drive one device: input, screenshots, live preview, apps, logs and inspection.',
|
|
110
|
+
},
|
|
111
|
+
{ name: 'Reservations', description: 'Reserving a device for a person for a while.' },
|
|
112
|
+
{
|
|
113
|
+
name: 'Leases',
|
|
114
|
+
description: 'Claiming devices from code (SDKs, MCP tools), with a heartbeat.',
|
|
115
|
+
},
|
|
116
|
+
{ name: 'Queue', description: 'Sessions waiting for a free device.' },
|
|
117
|
+
{
|
|
118
|
+
name: 'Hub-Node',
|
|
119
|
+
description: 'How nodes report their devices to a hub, and what a node answers its hub.',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'Sessions',
|
|
123
|
+
description: 'Appium sessions: their history, commands, logs, assets and performance.',
|
|
124
|
+
},
|
|
125
|
+
{ name: 'Builds', description: 'Groups of sessions from one test run.' },
|
|
126
|
+
{
|
|
127
|
+
name: 'Selector Health',
|
|
128
|
+
description: 'Selectors your tests found only with self-healing, and their fixes.',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'Network Interceptor',
|
|
132
|
+
description: "A session's captured HTTP traffic, mocks and HAR export.",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'Recordings',
|
|
136
|
+
description: 'Recordings of one or more devices, with marks, bookmarks and proof bundles.',
|
|
137
|
+
},
|
|
138
|
+
{ name: 'Applications', description: 'Uploaded app builds and which team sees them.' },
|
|
139
|
+
{ name: 'Webhooks', description: 'Where Xenon sends its events.' },
|
|
140
|
+
{ name: 'Configuration', description: 'Server settings, including AI providers.' },
|
|
141
|
+
{ name: 'Admin', description: 'Operations views for administrators.' },
|
|
68
142
|
],
|
|
69
143
|
components: {
|
|
70
144
|
schemas: {
|
|
71
|
-
|
|
145
|
+
Error: {
|
|
72
146
|
type: 'object',
|
|
147
|
+
description: 'An error. `error` is a machine-readable code on newer endpoints, otherwise the message itself (or `true`, with `message`).',
|
|
148
|
+
required: ['error'],
|
|
73
149
|
properties: {
|
|
74
|
-
|
|
75
|
-
type: 'string',
|
|
76
|
-
|
|
77
|
-
example: '00008110-00084CE80E51401E',
|
|
150
|
+
error: {
|
|
151
|
+
oneOf: [{ type: 'string' }, { type: 'boolean', enum: [true] }],
|
|
152
|
+
example: 'not_found',
|
|
78
153
|
},
|
|
154
|
+
message: { type: 'string', example: 'Device not found' },
|
|
155
|
+
},
|
|
156
|
+
additionalProperties: true,
|
|
157
|
+
},
|
|
158
|
+
Success: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: { success: { type: 'boolean', example: true } },
|
|
161
|
+
},
|
|
162
|
+
Device: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
description: 'A device in the lab, as the device list returns it.',
|
|
165
|
+
properties: {
|
|
166
|
+
udid: { type: 'string', example: '00008110-00084CE80E51401E' },
|
|
79
167
|
name: { type: 'string', example: 'iPhone 14 Pro' },
|
|
80
168
|
platform: { type: 'string', enum: ['ios', 'android'] },
|
|
81
|
-
host: {
|
|
169
|
+
host: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: 'The server the device is attached to.',
|
|
172
|
+
example: 'http://192.168.1.100:4723',
|
|
173
|
+
},
|
|
174
|
+
nodeId: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
nullable: true,
|
|
177
|
+
description: 'The node that reported it, on a hub.',
|
|
178
|
+
},
|
|
82
179
|
busy: { type: 'boolean' },
|
|
83
|
-
session_id: {
|
|
84
|
-
|
|
180
|
+
session_id: {
|
|
181
|
+
type: 'string',
|
|
182
|
+
nullable: true,
|
|
183
|
+
description: 'The Appium session on it, or a preview or recording hold (`manual_<userId>_<udid>`).',
|
|
184
|
+
},
|
|
185
|
+
state: { type: 'string', example: 'device' },
|
|
85
186
|
sdk: { type: 'string', example: '17.0' },
|
|
86
|
-
deviceType: { type: 'string' },
|
|
187
|
+
deviceType: { type: 'string', enum: ['real', 'simulator', 'emulator'] },
|
|
188
|
+
realDevice: { type: 'boolean' },
|
|
189
|
+
offline: { type: 'boolean' },
|
|
190
|
+
userBlocked: { type: 'boolean', description: 'Blocked for maintenance.' },
|
|
191
|
+
teamId: {
|
|
192
|
+
type: 'string',
|
|
193
|
+
nullable: true,
|
|
194
|
+
description: 'The owning team; null is the shared pool.',
|
|
195
|
+
},
|
|
196
|
+
reservedBy: { type: 'string', nullable: true },
|
|
197
|
+
reservedByUserId: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
nullable: true,
|
|
200
|
+
description: 'The user who took the reservation; null before 2.13.',
|
|
201
|
+
},
|
|
202
|
+
reservedUntil: { type: 'integer', nullable: true, description: 'Epoch milliseconds.' },
|
|
203
|
+
healthStatus: { type: 'string', example: 'Healthy' },
|
|
204
|
+
batteryLevel: { type: 'integer', nullable: true },
|
|
205
|
+
screenWidth: { type: 'string', nullable: true },
|
|
206
|
+
screenHeight: { type: 'string', nullable: true },
|
|
87
207
|
},
|
|
208
|
+
additionalProperties: true,
|
|
88
209
|
},
|
|
89
|
-
// ... (rest of schemas remain same for internal completeness)
|
|
90
210
|
Session: {
|
|
91
211
|
type: 'object',
|
|
92
212
|
properties: {
|
|
93
213
|
id: { type: 'string' },
|
|
94
|
-
name: { type: 'string' },
|
|
95
|
-
status: { type: 'string',
|
|
214
|
+
name: { type: 'string', nullable: true },
|
|
215
|
+
status: { type: 'string', example: 'success' },
|
|
216
|
+
build_id: { type: 'string', nullable: true },
|
|
96
217
|
device_udid: { type: 'string' },
|
|
97
218
|
device_name: { type: 'string' },
|
|
98
219
|
device_platform: { type: 'string' },
|
|
99
220
|
createdAt: { type: 'string', format: 'date-time' },
|
|
221
|
+
updatedAt: { type: 'string', format: 'date-time' },
|
|
100
222
|
},
|
|
223
|
+
additionalProperties: true,
|
|
101
224
|
},
|
|
102
225
|
Build: {
|
|
103
226
|
type: 'object',
|
|
@@ -110,95 +233,7 @@ Every authenticated response carries \`X-RateLimit-Limit\`, \`X-RateLimit-Remain
|
|
|
110
233
|
runningCount: { type: 'integer' },
|
|
111
234
|
createdAt: { type: 'string', format: 'date-time' },
|
|
112
235
|
},
|
|
113
|
-
|
|
114
|
-
Reservation: {
|
|
115
|
-
type: 'object',
|
|
116
|
-
properties: {
|
|
117
|
-
udid: { type: 'string' },
|
|
118
|
-
host: { type: 'string' },
|
|
119
|
-
reservedBy: { type: 'string' },
|
|
120
|
-
reservedUntil: { type: 'integer' },
|
|
121
|
-
reservationReason: { type: 'string' },
|
|
122
|
-
remainingMs: { type: 'integer' },
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
App: {
|
|
126
|
-
type: 'object',
|
|
127
|
-
properties: {
|
|
128
|
-
id: { type: 'string' },
|
|
129
|
-
name: { type: 'string' },
|
|
130
|
-
filename: { type: 'string' },
|
|
131
|
-
filepath: { type: 'string' },
|
|
132
|
-
platform: { type: 'string', enum: ['ios', 'android'] },
|
|
133
|
-
uploadedAt: { type: 'string', format: 'date-time' },
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
WebhookConfig: {
|
|
137
|
-
type: 'object',
|
|
138
|
-
properties: {
|
|
139
|
-
id: { type: 'string' },
|
|
140
|
-
url: { type: 'string', format: 'uri' },
|
|
141
|
-
events: { type: 'array', items: { type: 'string' } },
|
|
142
|
-
type: { type: 'string', enum: ['slack', 'webhook'] },
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
LocatorSuggestion: {
|
|
146
|
-
type: 'object',
|
|
147
|
-
properties: {
|
|
148
|
-
strategy: { type: 'string' },
|
|
149
|
-
value: { type: 'string' },
|
|
150
|
-
priority: { type: 'integer' },
|
|
151
|
-
isUnique: { type: 'boolean' },
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
InspectorNode: {
|
|
155
|
-
type: 'object',
|
|
156
|
-
properties: {
|
|
157
|
-
id: { type: 'string' },
|
|
158
|
-
type: { type: 'string' },
|
|
159
|
-
text: { type: 'string' },
|
|
160
|
-
path: { type: 'string' },
|
|
161
|
-
rect: {
|
|
162
|
-
type: 'object',
|
|
163
|
-
properties: {
|
|
164
|
-
x: { type: 'number' },
|
|
165
|
-
y: { type: 'number' },
|
|
166
|
-
width: { type: 'number' },
|
|
167
|
-
height: { type: 'number' },
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
attributes: { type: 'object', additionalProperties: true },
|
|
171
|
-
locators: {
|
|
172
|
-
type: 'array',
|
|
173
|
-
items: { $ref: '#/components/schemas/LocatorSuggestion' },
|
|
174
|
-
},
|
|
175
|
-
children: {
|
|
176
|
-
type: 'array',
|
|
177
|
-
items: { $ref: '#/components/schemas/InspectorNode' },
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
InspectorSnapshot: {
|
|
182
|
-
type: 'object',
|
|
183
|
-
properties: {
|
|
184
|
-
udid: { type: 'string' },
|
|
185
|
-
platform: { type: 'string' },
|
|
186
|
-
screenshot: { type: 'string', description: 'Base64 encoded screenshot' },
|
|
187
|
-
root: { $ref: '#/components/schemas/InspectorNode' },
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
Error: {
|
|
191
|
-
type: 'object',
|
|
192
|
-
properties: {
|
|
193
|
-
error: { type: 'boolean', example: true },
|
|
194
|
-
message: { type: 'string' },
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
Success: {
|
|
198
|
-
type: 'object',
|
|
199
|
-
properties: {
|
|
200
|
-
success: { type: 'boolean', example: true },
|
|
201
|
-
},
|
|
236
|
+
additionalProperties: true,
|
|
202
237
|
},
|
|
203
238
|
},
|
|
204
239
|
securitySchemes: {
|
|
@@ -206,77 +241,107 @@ Every authenticated response carries \`X-RateLimit-Limit\`, \`X-RateLimit-Remain
|
|
|
206
241
|
type: 'apiKey',
|
|
207
242
|
in: 'header',
|
|
208
243
|
name: 'x-xenon-access-key',
|
|
209
|
-
description: '
|
|
244
|
+
description: 'Your access key (`xen_…`), always sent together with `x-xenon-token`. Shown on your profile page, where you can also rotate it.',
|
|
210
245
|
},
|
|
211
246
|
TokenAuth: {
|
|
212
247
|
type: 'apiKey',
|
|
213
248
|
in: 'header',
|
|
214
249
|
name: 'x-xenon-token',
|
|
215
|
-
description: '
|
|
250
|
+
description: 'An API token from your profile page, sent together with `x-xenon-access-key`. It carries its own scopes.',
|
|
251
|
+
},
|
|
252
|
+
BearerAuth: {
|
|
253
|
+
type: 'http',
|
|
254
|
+
scheme: 'bearer',
|
|
255
|
+
bearerFormat: 'JWT',
|
|
256
|
+
description: 'A token from `POST /api/auth/token` (audience `xenon-rest`), valid for one hour. Verified against `/api/auth/jwks.json`; revoking its user takes effect at once.',
|
|
257
|
+
},
|
|
258
|
+
CookieAuth: {
|
|
259
|
+
type: 'apiKey',
|
|
260
|
+
in: 'cookie',
|
|
261
|
+
name: 'xenon_dashboard_session',
|
|
262
|
+
description: 'The dashboard session cookie set by `POST /api/auth/login`. State-changing requests also need a same-host `Origin` or `Referer`.',
|
|
263
|
+
},
|
|
264
|
+
HubToken: {
|
|
265
|
+
type: 'apiKey',
|
|
266
|
+
in: 'header',
|
|
267
|
+
name: 'x-xenon-hub-token',
|
|
268
|
+
description: "A hub's short-lived signed token, verified by the node against the hub's JWKS. Used only between a hub and its nodes.",
|
|
216
269
|
},
|
|
217
270
|
},
|
|
218
271
|
headers: {
|
|
219
|
-
'X-RateLimit-
|
|
220
|
-
description: '
|
|
221
|
-
schema: { type: '
|
|
272
|
+
'X-RateLimit-Category': {
|
|
273
|
+
description: 'The budget this request counted against: `read`, `heavy` or `control`.',
|
|
274
|
+
schema: { type: 'string', enum: ['read', 'heavy', 'control'] },
|
|
222
275
|
},
|
|
223
276
|
'X-RateLimit-Remaining': {
|
|
224
|
-
description: '
|
|
277
|
+
description: 'Requests left in that budget.',
|
|
225
278
|
schema: { type: 'integer' },
|
|
226
279
|
},
|
|
227
|
-
'X-RateLimit-
|
|
228
|
-
description: '
|
|
280
|
+
'X-RateLimit-Capacity': {
|
|
281
|
+
description: "The budget's size per minute.",
|
|
282
|
+
schema: { type: 'integer' },
|
|
283
|
+
},
|
|
284
|
+
'Retry-After': {
|
|
285
|
+
description: 'Seconds until a request in this budget will be accepted.',
|
|
229
286
|
schema: { type: 'integer' },
|
|
230
287
|
},
|
|
231
288
|
},
|
|
232
289
|
responses: {
|
|
290
|
+
BadRequest: {
|
|
291
|
+
description: 'The request is malformed or a field is missing or invalid.',
|
|
292
|
+
content: errorExample('bad_request', 'udid is required'),
|
|
293
|
+
},
|
|
233
294
|
Unauthorized: {
|
|
234
|
-
description: '
|
|
235
|
-
content:
|
|
236
|
-
'application/json': {
|
|
237
|
-
schema: { $ref: '#/components/schemas/Error' },
|
|
238
|
-
example: { error: true, message: 'Unauthorized' },
|
|
239
|
-
},
|
|
240
|
-
},
|
|
295
|
+
description: 'No credential, or one that is invalid, expired or revoked.',
|
|
296
|
+
content: errorExample('unauthenticated'),
|
|
241
297
|
},
|
|
242
298
|
Forbidden: {
|
|
243
|
-
description:
|
|
244
|
-
content:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
299
|
+
description: 'The credential is valid but lacks the scope or role this needs, or a browser request failed the same-origin check.',
|
|
300
|
+
content: errorExample('insufficient scope'),
|
|
301
|
+
},
|
|
302
|
+
NotFound: {
|
|
303
|
+
description: "It doesn't exist, or it is outside your teams (the two answer the same).",
|
|
304
|
+
content: errorExample('not_found', 'Not found'),
|
|
305
|
+
},
|
|
306
|
+
Conflict: {
|
|
307
|
+
description: 'It conflicts with the current state, for example a device held by another user.',
|
|
308
|
+
content: errorExample('device_held_by_another_user', 'This device is in use by another user.'),
|
|
250
309
|
},
|
|
251
310
|
RateLimited: {
|
|
252
|
-
description:
|
|
311
|
+
description: "The API key's budget for the request's category is spent. Only requests made with an access key and token are limited.",
|
|
253
312
|
headers: {
|
|
254
|
-
'X-RateLimit-
|
|
313
|
+
'X-RateLimit-Category': { $ref: '#/components/headers/X-RateLimit-Category' },
|
|
255
314
|
'X-RateLimit-Remaining': { $ref: '#/components/headers/X-RateLimit-Remaining' },
|
|
256
|
-
'X-RateLimit-
|
|
315
|
+
'X-RateLimit-Capacity': { $ref: '#/components/headers/X-RateLimit-Capacity' },
|
|
316
|
+
'Retry-After': { $ref: '#/components/headers/Retry-After' },
|
|
257
317
|
},
|
|
258
318
|
content: {
|
|
259
319
|
'application/json': {
|
|
260
320
|
schema: { $ref: '#/components/schemas/Error' },
|
|
261
|
-
example: { error:
|
|
321
|
+
example: { error: 'rate limit exceeded', category: 'control', retryAfter: 2 },
|
|
262
322
|
},
|
|
263
323
|
},
|
|
264
324
|
},
|
|
325
|
+
ServiceUnavailable: {
|
|
326
|
+
description: 'A dependency needed to answer could not be reached; try again.',
|
|
327
|
+
content: errorExample('internal', 'Service unavailable'),
|
|
328
|
+
},
|
|
329
|
+
InternalError: {
|
|
330
|
+
description: 'An unexpected server error.',
|
|
331
|
+
content: errorExample('internal', 'Internal server error'),
|
|
332
|
+
},
|
|
265
333
|
},
|
|
266
334
|
},
|
|
267
|
-
security: [{ AccessKeyAuth: [], TokenAuth: [] }],
|
|
335
|
+
security: [{ AccessKeyAuth: [], TokenAuth: [] }, { BearerAuth: [] }, { CookieAuth: [] }],
|
|
268
336
|
};
|
|
269
337
|
const path_1 = __importDefault(require("path"));
|
|
270
|
-
// @ts-ignore - Types will be available after package install
|
|
271
338
|
const options = {
|
|
272
339
|
swaggerDefinition,
|
|
273
340
|
apis: [
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
path_1.default.join(__dirname, '..', '..', '..', 'src', 'app', 'swagger-docs.ts'),
|
|
279
|
-
path_1.default.join(__dirname, '..', '..', '..', 'src', 'app', 'routers', '*.ts'),
|
|
341
|
+
// One YAML file per API area: src/app/openapi, copied to lib/src/app/openapi
|
|
342
|
+
// by build:copy. YAML, not JSDoc in a .ts file: tsc dropped most of those
|
|
343
|
+
// free-standing comments, and the served page lost 54 of its 100 paths.
|
|
344
|
+
path_1.default.join(__dirname, 'openapi', '*.yaml'),
|
|
280
345
|
],
|
|
281
346
|
};
|
|
282
347
|
const swaggerSpec = (0, swagger_jsdoc_1.default)(options);
|
|
@@ -425,6 +490,23 @@ function setupSwagger(app, basePath = '/xenon') {
|
|
|
425
490
|
.swagger-ui .dialog-ux .modal-ux-header h3 { font-family: 'Outfit', sans-serif; font-size: 28px; color: #fff !important; margin-bottom: 16px; }
|
|
426
491
|
.swagger-ui .dialog-ux .modal-ux-content h4 { color: var(--xenon-text-dim) !important; margin-top: 24px; }
|
|
427
492
|
|
|
493
|
+
/* Readability. The title's gradient text left the version badges blank, and
|
|
494
|
+
Swagger UI's own light section bars and dim greys sat on the dark page. */
|
|
495
|
+
.swagger-ui .info .title small, .swagger-ui .info .title small pre { -webkit-text-fill-color: #020617; color: #020617 !important; font-family: 'JetBrains Mono', monospace; }
|
|
496
|
+
.swagger-ui .info .markdown h1, .swagger-ui .info .markdown h2, .swagger-ui .info .markdown h3 { color: var(--xenon-text) !important; font-family: 'Outfit', sans-serif; margin-top: 40px; }
|
|
497
|
+
.swagger-ui .markdown p, .swagger-ui .markdown li, .swagger-ui .renderedMarkdown p, .swagger-ui .renderedMarkdown li { color: #cbd5e1 !important; }
|
|
498
|
+
.swagger-ui .markdown table td, .swagger-ui .markdown table th { color: #cbd5e1 !important; border-color: var(--xenon-border-bright) !important; padding: 8px 12px; vertical-align: top; }
|
|
499
|
+
.swagger-ui .markdown strong, .swagger-ui .renderedMarkdown strong { color: var(--xenon-text) !important; }
|
|
500
|
+
.swagger-ui .markdown a, .swagger-ui .info a { color: var(--xenon-blue) !important; }
|
|
501
|
+
.swagger-ui .opblock .opblock-section-header { background: var(--xenon-surface-hover) !important; box-shadow: none !important; border-top: 1px solid var(--xenon-border-bright); }
|
|
502
|
+
.swagger-ui .opblock .opblock-section-header h4, .swagger-ui .opblock .opblock-section-header label, .swagger-ui .opblock-title_normal { color: var(--xenon-text) !important; }
|
|
503
|
+
.swagger-ui .opblock-description-wrapper p, .swagger-ui .opblock-description-wrapper li, .swagger-ui .response-col_description, .swagger-ui .parameter__name, .swagger-ui .parameter__type, .swagger-ui .parameter__in, .swagger-ui table thead tr td, .swagger-ui table thead tr th, .swagger-ui .response-col_status, .swagger-ui .responses-inner h4, .swagger-ui .responses-inner h5, .swagger-ui .tab li, .swagger-ui .opblock-summary-description { color: #cbd5e1 !important; }
|
|
504
|
+
.swagger-ui .dialog-ux .modal-ux-content p, .swagger-ui .dialog-ux .modal-ux-content label, .swagger-ui .dialog-ux .modal-ux-content h4, .swagger-ui .dialog-ux .modal-ux-content h6 { color: #cbd5e1 !important; }
|
|
505
|
+
.swagger-ui .model, .swagger-ui .model-title, .swagger-ui .property-row td { color: #cbd5e1 !important; }
|
|
506
|
+
.swagger-ui .info h1, .swagger-ui .info h2, .swagger-ui .info h3, .swagger-ui .info h4 { color: var(--xenon-text) !important; font-family: 'Outfit', sans-serif; }
|
|
507
|
+
.swagger-ui .info table td, .swagger-ui .info table th, .swagger-ui .info li { color: #cbd5e1 !important; }
|
|
508
|
+
.swagger-ui table.headers td, .swagger-ui .header-row td, .swagger-ui .headers-wrapper td { color: #cbd5e1 !important; }
|
|
509
|
+
|
|
428
510
|
/* Custom Scrollbar */
|
|
429
511
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
430
512
|
::-webkit-scrollbar-track { background: var(--xenon-bg); }
|
|
@@ -454,13 +454,15 @@ function emitUnblocked(device) {
|
|
|
454
454
|
* @param reservedBy Username or identifier
|
|
455
455
|
* @param durationMs Duration in milliseconds
|
|
456
456
|
* @param reason Optional reservation reason
|
|
457
|
+
* @param reservedByUserId The user taking it, who alone (or an admin) may release or extend it
|
|
457
458
|
*/
|
|
458
|
-
function reserveDevice(udid, host, reservedBy, durationMs, reason) {
|
|
459
|
+
function reserveDevice(udid, host, reservedBy, durationMs, reason, reservedByUserId) {
|
|
459
460
|
return __awaiter(this, void 0, void 0, function* () {
|
|
460
461
|
const reservedUntil = Date.now() + durationMs;
|
|
461
462
|
logger_1.default.info(`Reserving device ${udid} for ${reservedBy} until ${new Date(reservedUntil).toISOString()}`);
|
|
462
463
|
yield store.updateDevice(udid, host, {
|
|
463
464
|
reservedBy,
|
|
465
|
+
reservedByUserId: reservedByUserId !== null && reservedByUserId !== void 0 ? reservedByUserId : null,
|
|
464
466
|
reservedUntil,
|
|
465
467
|
reservationReason: reason,
|
|
466
468
|
});
|
|
@@ -476,6 +478,7 @@ function releaseReservation(udid, host) {
|
|
|
476
478
|
logger_1.default.info(`Releasing reservation for device ${udid}`);
|
|
477
479
|
yield store.updateDevice(udid, host, {
|
|
478
480
|
reservedBy: null,
|
|
481
|
+
reservedByUserId: null,
|
|
479
482
|
reservedUntil: null,
|
|
480
483
|
reservationReason: null,
|
|
481
484
|
});
|
|
@@ -1043,9 +1043,12 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
1043
1043
|
}
|
|
1044
1044
|
getLogs(udid) {
|
|
1045
1045
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1046
|
+
// A failure throws. It used to come back as the log text itself, so
|
|
1047
|
+
// device control answered 200 with the error as a "line", and the
|
|
1048
|
+
// session log stored it as one.
|
|
1046
1049
|
const { adbInstance } = yield this.getAdb();
|
|
1047
1050
|
if (!adbInstance)
|
|
1048
|
-
|
|
1051
|
+
throw new Error('ADB is not available');
|
|
1049
1052
|
try {
|
|
1050
1053
|
// Get last 500 lines of logcat
|
|
1051
1054
|
return yield adbInstance.adbExec([
|
|
@@ -1062,7 +1065,7 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
1062
1065
|
}
|
|
1063
1066
|
catch (err) {
|
|
1064
1067
|
logger_1.default.warn(`Failed to fetch Android logs for ${udid}: ${err instanceof Error ? err.message : err}`);
|
|
1065
|
-
|
|
1068
|
+
throw err;
|
|
1066
1069
|
}
|
|
1067
1070
|
});
|
|
1068
1071
|
}
|