@xenon-device-management/xenon 2.14.0 → 2.15.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 +7 -4
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-jNQHb2IR.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BjGymyf3.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-Be7LNMXV.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-C-OtmIuk.js} +6 -6
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-BbKEXUFH.js → index-BOxe43p1.js} +2 -2
- package/lib/public/assets/{index-DJGc1M2f.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-Cm0tmSoc.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-t7qr3fvX.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-Cw_g218H.js} +1 -1
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-C3uyGhyc.js} +1 -1
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-BU-C9lBf.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +5 -5
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +6 -4
- package/lib/src/app/openapi/grid.yaml +8 -2
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +33 -15
- package/lib/src/app/openapi/sessions.yaml +10 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +29 -5
- package/lib/src/app/routers/control.js +13 -5
- package/lib/src/app/routers/dashboard.js +42 -27
- package/lib/src/app/routers/grid.js +5 -3
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/users.js +10 -5
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +49 -61
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/device-store.js +14 -1
- package/lib/src/data-service/prisma-store.js +19 -7
- package/lib/src/data-service/web-config-service.js +7 -0
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +20 -2
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +2 -0
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/sessionGateway.js +12 -2
- 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 +32 -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 +3 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/interceptors/CommandInterceptor.js +243 -181
- package/lib/src/middleware/authMiddleware.js +15 -3
- package/lib/src/middleware/commandCaller.js +3 -3
- package/lib/src/middleware/verifyCredential.js +58 -1
- package/lib/src/plugin.js +8 -3
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/OrphanSweeper.js +2 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +86 -26
- package/lib/src/services/SocketServer.js +19 -3
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +55 -72
- package/lib/src/services/healing/OcrHealingProvider.js +41 -64
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
- package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
- package/lib/src/services/healing/fingerprintIdentity.js +126 -0
- package/lib/src/services/healing/resilioPath.js +274 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -0
- package/lib/src/services/ocr/ocrData.js +118 -0
- package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
- package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
- package/lib/src/services/passwordResetLink.js +50 -9
- package/lib/src/services/passwordResetMode.js +3 -1
- package/lib/src/services/publicUrl.js +45 -0
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
- package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
- package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +244 -0
- package/lib/test/unit/failure-analysis.spec.js +182 -0
- package/lib/test/unit/failure-categories.spec.js +128 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +73 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +171 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/lease/lease-override.spec.js +23 -6
- package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
- package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
- package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
- package/lib/test/unit/ocr-offline.spec.js +213 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
- package/lib/test/unit/omni-vision.spec.js +5 -7
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/reset-link-address.spec.js +266 -0
- package/lib/test/unit/reset-link.test.js +10 -4
- package/lib/test/unit/resilio-path-healing.spec.js +270 -0
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +13 -13
- package/lib/test/unit/self-healing-boot.spec.js +10 -5
- package/lib/test/unit/self-healing-setting.spec.js +7 -6
- package/lib/test/unit/session-app-resolution.spec.js +5 -1
- package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
- package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
- package/lib/test/unit/session-credential-strip.spec.js +1 -1
- package/lib/test/unit/session-device-logs.spec.js +347 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
- package/lib/test/unit/verify-credential.spec.js +33 -0
- package/lib/test/unit/vision-assertion.spec.js +3 -2
- package/lib/test/unit/web-config-service.spec.js +19 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +5 -5
- package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
- package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
- package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
- package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
- package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -51,7 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
51
51
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
52
|
};
|
|
53
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
-
exports.SESSION_MANAGER = exports.SessionManager = void 0;
|
|
54
|
+
exports.SESSION_MANAGER = exports.SessionManager = exports.HUB_RESTART_CATEGORY = void 0;
|
|
55
55
|
const os_1 = __importDefault(require("os"));
|
|
56
56
|
const typedi_1 = require("typedi");
|
|
57
57
|
const prisma_1 = require("../prisma");
|
|
@@ -62,6 +62,12 @@ const device_store_1 = require("../data-service/device-store");
|
|
|
62
62
|
const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
|
|
63
63
|
const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
|
|
64
64
|
const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
|
|
65
|
+
/**
|
|
66
|
+
* The `failure_category` of a session a restart ended: one still running at
|
|
67
|
+
* boot that couldn't be picked up again. The failure analysis writes every
|
|
68
|
+
* other category (`ANALYSIS_CATEGORIES`).
|
|
69
|
+
*/
|
|
70
|
+
exports.HUB_RESTART_CATEGORY = 'HUB_RESTART';
|
|
65
71
|
/**
|
|
66
72
|
* SessionManager with persistence and recovery capabilities.
|
|
67
73
|
*
|
|
@@ -306,7 +312,7 @@ let SessionManager = class SessionManager {
|
|
|
306
312
|
status: 'failed',
|
|
307
313
|
endTime: new Date(),
|
|
308
314
|
failure_reason: reason,
|
|
309
|
-
failure_category:
|
|
315
|
+
failure_category: exports.HUB_RESTART_CATEGORY,
|
|
310
316
|
},
|
|
311
317
|
});
|
|
312
318
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.forgetSessionMemory = forgetSessionMemory;
|
|
4
|
+
const typedi_1 = require("typedi");
|
|
5
|
+
const OmniVisionService_1 = require("../services/omni-vision/OmniVisionService");
|
|
6
|
+
const AutowaitService_1 = require("../services/autowait/AutowaitService");
|
|
7
|
+
/**
|
|
8
|
+
* Drop what this server keeps in memory for a session's commands: the
|
|
9
|
+
* elements its finds and heals found in screenshots (OmniVisionService) and
|
|
10
|
+
* its autowait settings (AutowaitService). Called however the session ends
|
|
11
|
+
* here: the test's delete (SessionLifecycleService.deleteSession), Appium's
|
|
12
|
+
* new-command timeout or a crash (XenonPlugin.onUnexpectedShutdown), the idle
|
|
13
|
+
* release (releaseBlockedDevices), a stale heartbeat (OrphanSweeper) and a
|
|
14
|
+
* shutdown's drain (stopSessionForShutdown). Safe to call more than once.
|
|
15
|
+
*
|
|
16
|
+
* Through 2.14 the elements were never dropped, and the autowait settings
|
|
17
|
+
* only by the interceptor on `deleteSession`, a command Appium never hands
|
|
18
|
+
* it: XenonPlugin answers `deleteSession` itself.
|
|
19
|
+
*/
|
|
20
|
+
function forgetSessionMemory(sessionId) {
|
|
21
|
+
if (!sessionId)
|
|
22
|
+
return;
|
|
23
|
+
typedi_1.Container.get(OmniVisionService_1.OmniVisionService).forgetSession(sessionId);
|
|
24
|
+
typedi_1.Container.get(AutowaitService_1.AutowaitService).clearSession(sessionId);
|
|
25
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.OLLAMA_URL = exports.PROVIDERS = void 0;
|
|
16
|
+
exports.useFakeAiProviders = useFakeAiProviders;
|
|
17
|
+
const axios_1 = __importDefault(require("axios"));
|
|
18
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
19
|
+
const config_1 = require("../../src/config");
|
|
20
|
+
exports.PROVIDERS = ['gemini', 'openai', 'anthropic', 'ollama'];
|
|
21
|
+
const AI_SETTINGS = [
|
|
22
|
+
'aiProvider',
|
|
23
|
+
'aiModel',
|
|
24
|
+
'aiBaseUrl',
|
|
25
|
+
'geminiApiKey',
|
|
26
|
+
'openaiApiKey',
|
|
27
|
+
'anthropicApiKey',
|
|
28
|
+
'geminiModel',
|
|
29
|
+
'openaiModel',
|
|
30
|
+
'anthropicModel',
|
|
31
|
+
'ollamaModel',
|
|
32
|
+
];
|
|
33
|
+
exports.OLLAMA_URL = 'http://ollama.test:11434';
|
|
34
|
+
function json(status, body) {
|
|
35
|
+
return new Response(JSON.stringify(body), {
|
|
36
|
+
status,
|
|
37
|
+
// The OpenAI and Anthropic SDKs retry a 429 after a pause unless told not to.
|
|
38
|
+
headers: { 'content-type': 'application/json', 'x-should-retry': 'false' },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function okBody(provider, text) {
|
|
42
|
+
switch (provider) {
|
|
43
|
+
case 'gemini':
|
|
44
|
+
return {
|
|
45
|
+
candidates: [
|
|
46
|
+
{ content: { role: 'model', parts: [{ text }] }, finishReason: 'STOP', index: 0 },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
case 'openai':
|
|
50
|
+
return {
|
|
51
|
+
id: 'chatcmpl-test',
|
|
52
|
+
object: 'chat.completion',
|
|
53
|
+
created: 0,
|
|
54
|
+
model: 'test',
|
|
55
|
+
choices: [
|
|
56
|
+
{ index: 0, message: { role: 'assistant', content: text }, finish_reason: 'stop' },
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
case 'anthropic':
|
|
60
|
+
return {
|
|
61
|
+
id: 'msg_test',
|
|
62
|
+
type: 'message',
|
|
63
|
+
role: 'assistant',
|
|
64
|
+
model: 'test',
|
|
65
|
+
content: [{ type: 'text', text }],
|
|
66
|
+
stop_reason: 'end_turn',
|
|
67
|
+
usage: { input_tokens: 1, output_tokens: 1 },
|
|
68
|
+
};
|
|
69
|
+
default:
|
|
70
|
+
return { response: text };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function errorBody(provider, status, said) {
|
|
74
|
+
const message = said !== null && said !== void 0 ? said : (status === 429 ? 'Resource has been exhausted (e.g. check quota).' : `HTTP ${status}`);
|
|
75
|
+
switch (provider) {
|
|
76
|
+
case 'gemini':
|
|
77
|
+
return { error: { code: status, message, status: 'RESOURCE_EXHAUSTED' } };
|
|
78
|
+
case 'anthropic':
|
|
79
|
+
return { type: 'error', error: { type: 'rate_limit_error', message } };
|
|
80
|
+
default:
|
|
81
|
+
return { error: { message, type: 'requests', code: 'rate_limit_exceeded' } };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function aborted() {
|
|
85
|
+
const err = new Error('This operation was aborted');
|
|
86
|
+
err.name = 'AbortError';
|
|
87
|
+
return err;
|
|
88
|
+
}
|
|
89
|
+
/** Resolves never, and rejects as fetch does when the request's signal aborts. */
|
|
90
|
+
function hangUntilAborted(signal) {
|
|
91
|
+
return new Promise((_, reject) => {
|
|
92
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
|
|
93
|
+
return reject(aborted());
|
|
94
|
+
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', () => reject(aborted()));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function useFakeAiProviders() {
|
|
98
|
+
let saved = {};
|
|
99
|
+
let savedMockEnv;
|
|
100
|
+
let provider = 'gemini';
|
|
101
|
+
let current = { status: 500 };
|
|
102
|
+
let ollamaUp = true;
|
|
103
|
+
const calls = [];
|
|
104
|
+
const sandbox = sinon_1.default.createSandbox();
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
saved = Object.fromEntries(AI_SETTINGS.map((k) => [k, config_1.config[k]]));
|
|
107
|
+
savedMockEnv = process.env.GEMINI_API_KEY;
|
|
108
|
+
// 'mock' would put the service in its built-in mock mode.
|
|
109
|
+
if (process.env.GEMINI_API_KEY === 'mock')
|
|
110
|
+
delete process.env.GEMINI_API_KEY;
|
|
111
|
+
for (const k of AI_SETTINGS)
|
|
112
|
+
config_1.config[k] = undefined;
|
|
113
|
+
config_1.config.aiProvider = 'gemini';
|
|
114
|
+
calls.length = 0;
|
|
115
|
+
current = { status: 500 };
|
|
116
|
+
ollamaUp = true;
|
|
117
|
+
sandbox.stub(globalThis, 'fetch').callsFake(((_url, init) => __awaiter(this, void 0, void 0, function* () {
|
|
118
|
+
var _a, _b;
|
|
119
|
+
const signal = (_a = init === null || init === void 0 ? void 0 : init.signal) !== null && _a !== void 0 ? _a : undefined;
|
|
120
|
+
calls.push({ provider, body: String((_b = init === null || init === void 0 ? void 0 : init.body) !== null && _b !== void 0 ? _b : ''), signal });
|
|
121
|
+
if (current === 'hang')
|
|
122
|
+
return hangUntilAborted(signal);
|
|
123
|
+
if ('text' in current)
|
|
124
|
+
return json(200, okBody(provider, current.text));
|
|
125
|
+
return json(current.status, errorBody(provider, current.status, current.message));
|
|
126
|
+
})));
|
|
127
|
+
// Ollama: its health check answers, then the generate call as told.
|
|
128
|
+
sandbox.stub(axios_1.default, 'get').callsFake(((url) => __awaiter(this, void 0, void 0, function* () {
|
|
129
|
+
if (url.startsWith(exports.OLLAMA_URL)) {
|
|
130
|
+
if (ollamaUp)
|
|
131
|
+
return { status: 200, data: { models: [] } };
|
|
132
|
+
throw Object.assign(new Error(`connect ECONNREFUSED ${exports.OLLAMA_URL}`), {
|
|
133
|
+
code: 'ECONNREFUSED',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`no network in tests: GET ${url}`);
|
|
137
|
+
})));
|
|
138
|
+
sandbox.stub(axios_1.default, 'post').callsFake(((url, body, cfg) => __awaiter(this, void 0, void 0, function* () {
|
|
139
|
+
if (!url.startsWith(exports.OLLAMA_URL))
|
|
140
|
+
throw new Error(`no network in tests: POST ${url}`);
|
|
141
|
+
const signal = cfg === null || cfg === void 0 ? void 0 : cfg.signal;
|
|
142
|
+
calls.push({ provider: 'ollama', body: JSON.stringify(body), signal });
|
|
143
|
+
if (current === 'hang')
|
|
144
|
+
return hangUntilAborted(signal);
|
|
145
|
+
if ('text' in current)
|
|
146
|
+
return { status: 200, data: okBody('ollama', current.text) };
|
|
147
|
+
const err = new Error(`Request failed with status code ${current.status}`);
|
|
148
|
+
err.response = {
|
|
149
|
+
status: current.status,
|
|
150
|
+
data: errorBody('ollama', current.status, current.message),
|
|
151
|
+
};
|
|
152
|
+
throw err;
|
|
153
|
+
})));
|
|
154
|
+
});
|
|
155
|
+
afterEach(() => {
|
|
156
|
+
sandbox.restore();
|
|
157
|
+
for (const k of AI_SETTINGS)
|
|
158
|
+
config_1.config[k] = saved[k];
|
|
159
|
+
if (savedMockEnv === undefined)
|
|
160
|
+
delete process.env.GEMINI_API_KEY;
|
|
161
|
+
else
|
|
162
|
+
process.env.GEMINI_API_KEY = savedMockEnv;
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
calls,
|
|
166
|
+
use(name, opts = {}) {
|
|
167
|
+
provider = name;
|
|
168
|
+
const model = `test-${name}-${Math.random().toString(36).slice(2)}`;
|
|
169
|
+
const key = opts.key === false ? undefined : 'test-key';
|
|
170
|
+
config_1.config.aiProvider = name;
|
|
171
|
+
if (name === 'gemini')
|
|
172
|
+
Object.assign(config_1.config, { geminiApiKey: key, geminiModel: model });
|
|
173
|
+
if (name === 'openai')
|
|
174
|
+
Object.assign(config_1.config, { openaiApiKey: key, openaiModel: model });
|
|
175
|
+
if (name === 'anthropic')
|
|
176
|
+
Object.assign(config_1.config, { anthropicApiKey: key, anthropicModel: model });
|
|
177
|
+
if (name === 'ollama')
|
|
178
|
+
Object.assign(config_1.config, { aiBaseUrl: exports.OLLAMA_URL, ollamaModel: model });
|
|
179
|
+
return { model };
|
|
180
|
+
},
|
|
181
|
+
answer(answer) {
|
|
182
|
+
current = answer;
|
|
183
|
+
},
|
|
184
|
+
ollamaUp(up) {
|
|
185
|
+
ollamaUp = up;
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.useLokiStores = useLokiStores;
|
|
4
|
+
exports.usePrismaStores = usePrismaStores;
|
|
4
5
|
const device_store_1 = require("../../src/data-service/device-store");
|
|
6
|
+
const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
5
7
|
// DeviceStoreFactory's cached stores, one per kind.
|
|
6
8
|
const CACHES = ['_deviceStore', '_pendingSessionStore', '_cliArgsStore', '_healEtalonStore'];
|
|
7
9
|
/**
|
|
@@ -39,3 +41,34 @@ function useLokiStores() {
|
|
|
39
41
|
process.env.NODE_ENV = previousEnv;
|
|
40
42
|
});
|
|
41
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The Prisma stores (what a server with SQLite runs) for the enclosing
|
|
46
|
+
* `describe`, however the file was started. Put the suite's database behind
|
|
47
|
+
* them first: `useScratchDatabase({ wholeSuite: true })`.
|
|
48
|
+
*
|
|
49
|
+
* `npm run test:all` sets NODE_ENV to 'test', so DeviceStoreFactory hands it
|
|
50
|
+
* Loki stores, while `npx mocha <file>` gets Prisma ones. A spec written
|
|
51
|
+
* against the Prisma store's queries then tested Loki in the full run, and its
|
|
52
|
+
* phones stayed in Loki's one collection for every later spec. This hands the
|
|
53
|
+
* factory new Prisma stores for the suite, then puts the old ones back. A
|
|
54
|
+
* module that took its store when it was imported (`grid.ts`) keeps that one.
|
|
55
|
+
*
|
|
56
|
+
* Call it inside a `describe`, before the hooks that use a store, never at the
|
|
57
|
+
* top of a file.
|
|
58
|
+
*/
|
|
59
|
+
function usePrismaStores() {
|
|
60
|
+
const factory = device_store_1.DeviceStoreFactory;
|
|
61
|
+
const saved = {};
|
|
62
|
+
before(() => {
|
|
63
|
+
for (const key of CACHES)
|
|
64
|
+
saved[key] = factory[key];
|
|
65
|
+
factory._deviceStore = new prisma_store_1.PrismaDeviceStore();
|
|
66
|
+
factory._pendingSessionStore = new prisma_store_1.PrismaPendingSessionStore();
|
|
67
|
+
factory._cliArgsStore = new prisma_store_1.PrismaCLIArgsStore();
|
|
68
|
+
factory._healEtalonStore = new prisma_store_1.PrismaHealEtalonStore();
|
|
69
|
+
});
|
|
70
|
+
after(() => {
|
|
71
|
+
for (const key of CACHES)
|
|
72
|
+
factory[key] = saved[key];
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ios = exports.android = void 0;
|
|
13
|
+
exports.elementIdOf = elementIdOf;
|
|
14
|
+
exports.pageDriver = pageDriver;
|
|
15
|
+
const xmldom_1 = require("@xmldom/xmldom");
|
|
16
|
+
const xpath_1 = require("xpath");
|
|
17
|
+
/**
|
|
18
|
+
* Page sources shaped like the drivers' own (UiAutomator2's `hierarchy`,
|
|
19
|
+
* XCUITest's `AppiumAUT`), and a driver that answers finds by running the
|
|
20
|
+
* XPath on them, as the real drivers do, for specs that check which element a
|
|
21
|
+
* healed locator selects. No phone.
|
|
22
|
+
*/
|
|
23
|
+
const W3C_ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf';
|
|
24
|
+
const attrs = (o) => Object.entries(o)
|
|
25
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
26
|
+
.join(' ');
|
|
27
|
+
function androidNode(tag, index, bounds, extra = {}, children) {
|
|
28
|
+
const a = attrs(Object.assign({ index, package: 'com.example.shop', class: tag, text: '', 'resource-id': '', 'content-desc': '', checkable: 'false', checked: 'false', clickable: 'false', enabled: 'true', focusable: 'false', focused: 'false', 'long-clickable': 'false', password: 'false', scrollable: 'false', selected: 'false', bounds, displayed: 'true' }, extra));
|
|
29
|
+
return children === undefined ? `<${tag} ${a} />` : `<${tag} ${a}>${children}</${tag}>`;
|
|
30
|
+
}
|
|
31
|
+
exports.android = {
|
|
32
|
+
button: (index, id, text, bounds) => androidNode('android.widget.Button', index, bounds, {
|
|
33
|
+
'resource-id': `com.example.shop:id/${id}`,
|
|
34
|
+
text,
|
|
35
|
+
'content-desc': text,
|
|
36
|
+
clickable: 'true',
|
|
37
|
+
focusable: 'true',
|
|
38
|
+
}),
|
|
39
|
+
field: (index, id, text, bounds) => androidNode('android.widget.EditText', index, bounds, {
|
|
40
|
+
'resource-id': `com.example.shop:id/${id}`,
|
|
41
|
+
text,
|
|
42
|
+
clickable: 'true',
|
|
43
|
+
focusable: 'true',
|
|
44
|
+
}),
|
|
45
|
+
text: (index, id, text, bounds) => androidNode('android.widget.TextView', index, bounds, {
|
|
46
|
+
'resource-id': `com.example.shop:id/${id}`,
|
|
47
|
+
text,
|
|
48
|
+
}),
|
|
49
|
+
image: (index, id, description, bounds) => androidNode('android.widget.ImageView', index, bounds, {
|
|
50
|
+
'resource-id': `com.example.shop:id/${id}`,
|
|
51
|
+
'content-desc': description,
|
|
52
|
+
}),
|
|
53
|
+
/** A clickable row holding other elements, as list rows and compound buttons are. */
|
|
54
|
+
row: (index, id, bounds, children) => androidNode('android.widget.LinearLayout', index, bounds, { 'resource-id': `com.example.shop:id/${id}`, clickable: 'true', focusable: 'true' }, children),
|
|
55
|
+
layout: (index, id, bounds, children) => androidNode('android.widget.LinearLayout', index, bounds, { 'resource-id': id ? `com.example.shop:id/${id}` : '' }, children),
|
|
56
|
+
page: (children) => "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>" +
|
|
57
|
+
'<hierarchy index="0" class="hierarchy" rotation="0" width="1080" height="2220">' +
|
|
58
|
+
androidNode('android.widget.FrameLayout', 0, '[0,0][1080,2220]', {}, children) +
|
|
59
|
+
'</hierarchy>',
|
|
60
|
+
};
|
|
61
|
+
function iosNode(type, a, children = '') {
|
|
62
|
+
return `<${type} ${attrs(Object.assign({ type, enabled: 'true', visible: 'true', accessible: 'true' }, a))}>${children}</${type}>`;
|
|
63
|
+
}
|
|
64
|
+
exports.ios = {
|
|
65
|
+
button: (name, label, y) => iosNode('XCUIElementTypeButton', { name, label, x: 20, y, width: 350, height: 44 }),
|
|
66
|
+
field: (name, value, y) => iosNode('XCUIElementTypeTextField', { name, value, x: 20, y, width: 350, height: 44 }),
|
|
67
|
+
group: (children) => iosNode('XCUIElementTypeOther', { x: 0, y: 100, width: 390, height: 600 }, children),
|
|
68
|
+
page: (children) => '<?xml version="1.0" encoding="UTF-8"?><AppiumAUT>' +
|
|
69
|
+
iosNode('XCUIElementTypeApplication', { name: 'Shop', label: 'Shop', x: 0, y: 0, width: 390, height: 844 }, iosNode('XCUIElementTypeWindow', { x: 0, y: 0, width: 390, height: 844 }, children)) +
|
|
70
|
+
'</AppiumAUT>',
|
|
71
|
+
};
|
|
72
|
+
/** The element id the fake driver gives an element: its resource-id or name. */
|
|
73
|
+
function elementIdOf(el) {
|
|
74
|
+
return `el:${el.getAttribute('resource-id') || el.getAttribute('name') || el.tagName}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A driver showing `pageSource`: finds run the XPath on it and answer the
|
|
78
|
+
* first match, attributes and rects are the element's own. `show` changes the
|
|
79
|
+
* screen.
|
|
80
|
+
*/
|
|
81
|
+
function pageDriver(pageSource) {
|
|
82
|
+
let doc = new xmldom_1.DOMParser().parseFromString(pageSource, 'text/xml');
|
|
83
|
+
let source = pageSource;
|
|
84
|
+
const known = new Map();
|
|
85
|
+
const select = (using, value) => {
|
|
86
|
+
if (using !== 'xpath')
|
|
87
|
+
throw new Error(`unsupported locator strategy ${using}`);
|
|
88
|
+
return (0, xpath_1.select)(value, doc).filter((n) => n.nodeType === 1);
|
|
89
|
+
};
|
|
90
|
+
const ref = (el) => {
|
|
91
|
+
const id = elementIdOf(el);
|
|
92
|
+
known.set(id, el);
|
|
93
|
+
return { ELEMENT: id, [W3C_ELEMENT_KEY]: id };
|
|
94
|
+
};
|
|
95
|
+
const element = (id) => {
|
|
96
|
+
const el = known.get(id);
|
|
97
|
+
if (!el)
|
|
98
|
+
throw new Error(`no element ${id}`);
|
|
99
|
+
return el;
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
show(next) {
|
|
103
|
+
source = next;
|
|
104
|
+
doc = new xmldom_1.DOMParser().parseFromString(next, 'text/xml');
|
|
105
|
+
},
|
|
106
|
+
getPageSource() {
|
|
107
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
108
|
+
return source;
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
findElement(using, value) {
|
|
112
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
const [first] = select(using, value);
|
|
114
|
+
if (!first) {
|
|
115
|
+
throw Object.assign(new Error('NoSuchElement: An element could not be located'), {
|
|
116
|
+
name: 'NoSuchElementError',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return ref(first);
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
findElements(using, value) {
|
|
123
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
124
|
+
return select(using, value).map(ref);
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
caps: { platformName: /<XCUIElementType/.test(pageSource) ? 'iOS' : 'Android' },
|
|
128
|
+
/**
|
|
129
|
+
* As the drivers answer: getAttribute(name, elementId). UiAutomator2
|
|
130
|
+
* answers String(value), so an attribute the element hasn't set is
|
|
131
|
+
* "null"; WebDriverAgent answers null.
|
|
132
|
+
*/
|
|
133
|
+
getAttribute(name, id) {
|
|
134
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
135
|
+
const value = element(id).getAttribute(name);
|
|
136
|
+
if (value)
|
|
137
|
+
return value;
|
|
138
|
+
return /<XCUIElementType/.test(source) ? null : 'null';
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
getName(id) {
|
|
142
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
143
|
+
return element(id).tagName;
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
getElementRect(id) {
|
|
147
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
148
|
+
var _a;
|
|
149
|
+
const el = element(id);
|
|
150
|
+
const bounds = el.getAttribute('bounds');
|
|
151
|
+
if (bounds) {
|
|
152
|
+
const [x1, y1, x2, y2] = ((_a = bounds.match(/\d+/g)) !== null && _a !== void 0 ? _a : []).map(Number);
|
|
153
|
+
return { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
x: Number(el.getAttribute('x')),
|
|
157
|
+
y: Number(el.getAttribute('y')),
|
|
158
|
+
width: Number(el.getAttribute('width')),
|
|
159
|
+
height: Number(el.getAttribute('height')),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -52,16 +52,54 @@ const MODELS = [
|
|
|
52
52
|
'sessionMetric',
|
|
53
53
|
'selectorEvent',
|
|
54
54
|
];
|
|
55
|
+
let scratchCount = 0;
|
|
56
|
+
let template;
|
|
57
|
+
/**
|
|
58
|
+
* A database migrated once for the whole process, which each suite copies.
|
|
59
|
+
* Removed when the process exits.
|
|
60
|
+
*/
|
|
61
|
+
function migratedTemplate() {
|
|
62
|
+
if (template)
|
|
63
|
+
return template;
|
|
64
|
+
const file = path_1.default.join(os_1.default.tmpdir(), `xenon-scratch-template-${process.pid}-${Date.now()}.db`);
|
|
65
|
+
(0, child_process_1.execSync)('npx prisma migrate deploy', {
|
|
66
|
+
cwd: path_1.default.resolve(__dirname, '../..'),
|
|
67
|
+
env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: `file:${file}` }),
|
|
68
|
+
stdio: 'pipe',
|
|
69
|
+
});
|
|
70
|
+
process.once('exit', () => {
|
|
71
|
+
for (const f of [file, `${file}-journal`]) {
|
|
72
|
+
try {
|
|
73
|
+
fs_1.default.unlinkSync(f);
|
|
74
|
+
}
|
|
75
|
+
catch (_a) {
|
|
76
|
+
// already gone
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
template = file;
|
|
81
|
+
return file;
|
|
82
|
+
}
|
|
55
83
|
/**
|
|
56
84
|
* Back the whole `prisma` wrapper with a scratch SQLite file for the enclosing
|
|
57
85
|
* `describe`, so the real queries (the device store, the session lifecycle,
|
|
58
86
|
* SessionOwnerResolver) run against a real database that is not the server's.
|
|
59
87
|
* Like useScratchPortLeases, for every model.
|
|
60
88
|
*
|
|
61
|
-
* Call it inside a `describe`, never at the top of a file.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
89
|
+
* Call it inside a `describe`, never at the top of a file. Each suite gets a
|
|
90
|
+
* fresh database: a copy of one migrated once per process (migrating takes
|
|
91
|
+
* seconds). The stubs are made before each test and removed after it, so a
|
|
92
|
+
* spec's own `sinon.restore()` can't leave the next test on the real
|
|
93
|
+
* database. Tests empty the tables they use.
|
|
94
|
+
*
|
|
95
|
+
* With `wholeSuite`, the stubs are made once, before the suite's own `before`
|
|
96
|
+
* hooks (call it first in the describe), and removed after the suite. Making
|
|
97
|
+
* them takes tens of milliseconds, which a suite of hundreds of short tests
|
|
98
|
+
* otherwise pays per test. Its `before` hooks can then seed the rows every
|
|
99
|
+
* test reads, and nothing needs deleting: the file goes with the suite. Only
|
|
100
|
+
* for a suite that doesn't stub or restore `prisma` itself. Its own `after`
|
|
101
|
+
* hooks run once the stubs are gone, so they must not touch `prisma`: it is
|
|
102
|
+
* the real database again by then.
|
|
65
103
|
*
|
|
66
104
|
* With `captureQueries`, `queries` holds the SQL each test ran, so a spec can
|
|
67
105
|
* ask SQLite how it reads a table (`EXPLAIN QUERY PLAN`).
|
|
@@ -72,13 +110,10 @@ function useScratchDatabase(options = {}) {
|
|
|
72
110
|
let dbPath = '';
|
|
73
111
|
before(function () {
|
|
74
112
|
this.timeout(90000);
|
|
75
|
-
|
|
113
|
+
scratchCount += 1;
|
|
114
|
+
dbPath = path_1.default.join(os_1.default.tmpdir(), `xenon-scratch-${process.pid}-${Date.now()}-${scratchCount}.db`);
|
|
115
|
+
fs_1.default.copyFileSync(migratedTemplate(), dbPath);
|
|
76
116
|
const url = `file:${dbPath}`;
|
|
77
|
-
(0, child_process_1.execSync)('npx prisma migrate deploy', {
|
|
78
|
-
cwd: path_1.default.resolve(__dirname, '../..'),
|
|
79
|
-
env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: url }),
|
|
80
|
-
stdio: 'pipe',
|
|
81
|
-
});
|
|
82
117
|
if (options.captureQueries) {
|
|
83
118
|
const db = new client_1.PrismaClient({
|
|
84
119
|
datasources: { db: { url } },
|
|
@@ -90,9 +125,10 @@ function useScratchDatabase(options = {}) {
|
|
|
90
125
|
else {
|
|
91
126
|
ctx.db = new client_1.PrismaClient({ datasources: { db: { url } } });
|
|
92
127
|
}
|
|
128
|
+
if (options.wholeSuite)
|
|
129
|
+
stubPrisma();
|
|
93
130
|
});
|
|
94
|
-
|
|
95
|
-
ctx.queries.length = 0;
|
|
131
|
+
function stubPrisma() {
|
|
96
132
|
for (const model of MODELS) {
|
|
97
133
|
const wrapper = prisma_1.prisma[model];
|
|
98
134
|
const delegate = ctx.db[model];
|
|
@@ -102,8 +138,16 @@ function useScratchDatabase(options = {}) {
|
|
|
102
138
|
sandbox.stub(wrapper, name).callsFake((...args) => delegate[name](...args));
|
|
103
139
|
}
|
|
104
140
|
}
|
|
141
|
+
}
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
ctx.queries.length = 0;
|
|
144
|
+
if (!options.wholeSuite)
|
|
145
|
+
stubPrisma();
|
|
146
|
+
});
|
|
147
|
+
afterEach(() => {
|
|
148
|
+
if (!options.wholeSuite)
|
|
149
|
+
sandbox.restore();
|
|
105
150
|
});
|
|
106
|
-
afterEach(() => sandbox.restore());
|
|
107
151
|
after(() => __awaiter(this, void 0, void 0, function* () {
|
|
108
152
|
sandbox.restore();
|
|
109
153
|
if (ctx.db)
|
|
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
19
|
const sinon_1 = __importDefault(require("sinon"));
|
|
20
20
|
// Static import (matches the passing integration-spec idiom, e.g. selector-health-endpoint.spec):
|
|
21
21
|
// a dynamic `await import('...routers/audit')` fails ESM resolution on the extensionless path
|
|
@@ -37,7 +37,7 @@ describe('POST /xenon/api/audit/events', () => {
|
|
|
37
37
|
app.use('/xenon/api/audit', (0, audit_1.makeRouter)({ eventLogService: eventLogSvc }));
|
|
38
38
|
});
|
|
39
39
|
it('202s on a valid batch of 2 events and appends each via EventLogService.appendSafe with type mcp_audit', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
40
|
-
const res = yield (0,
|
|
40
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
41
41
|
.post('/xenon/api/audit/events')
|
|
42
42
|
.send({
|
|
43
43
|
events: [
|
|
@@ -70,20 +70,20 @@ describe('POST /xenon/api/audit/events', () => {
|
|
|
70
70
|
(0, chai_1.expect)(secondArg.payload).to.deep.include({ sessionId: 'sess-1' });
|
|
71
71
|
}));
|
|
72
72
|
it('400s when events is missing', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
73
|
-
const res = yield (0,
|
|
73
|
+
const res = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/audit/events').send({});
|
|
74
74
|
(0, chai_1.expect)(res.status).to.equal(400);
|
|
75
75
|
(0, chai_1.expect)(res.body.error).to.equal('bad_request');
|
|
76
76
|
(0, chai_1.expect)(eventLogSvc.appendSafe.called).to.be.false;
|
|
77
77
|
}));
|
|
78
78
|
it('400s when events is not an array', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
|
-
const res = yield (0,
|
|
79
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
80
80
|
.post('/xenon/api/audit/events')
|
|
81
81
|
.send({ events: 'nope' });
|
|
82
82
|
(0, chai_1.expect)(res.status).to.equal(400);
|
|
83
83
|
(0, chai_1.expect)(res.body.error).to.equal('bad_request');
|
|
84
84
|
}));
|
|
85
85
|
it('400s when a batch event is malformed (missing tool)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
-
const res = yield (0,
|
|
86
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
87
87
|
.post('/xenon/api/audit/events')
|
|
88
88
|
.send({
|
|
89
89
|
events: [{ subject: 'agent-1', decision: 'allow', latencyMs: 10 }],
|
|
@@ -100,7 +100,7 @@ describe('POST /xenon/api/audit/events', () => {
|
|
|
100
100
|
decision: 'allow',
|
|
101
101
|
latencyMs: i,
|
|
102
102
|
}));
|
|
103
|
-
const res = yield (0,
|
|
103
|
+
const res = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/audit/events').send({ events });
|
|
104
104
|
(0, chai_1.expect)(res.status).to.equal(400);
|
|
105
105
|
(0, chai_1.expect)(res.body.error).to.equal('bad_request');
|
|
106
106
|
(0, chai_1.expect)(eventLogSvc.appendSafe.called).to.be.false;
|
|
@@ -131,12 +131,12 @@ describe('POST /xenon/api/audit/events — guard (router-level roleGuard(MEMBER)
|
|
|
131
131
|
});
|
|
132
132
|
it('401s when unauthenticated', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
133
133
|
const app = yield makeApp(eventLogSvc)();
|
|
134
|
-
const res = yield (0,
|
|
134
|
+
const res = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/audit/events').send(validBody);
|
|
135
135
|
(0, chai_1.expect)(res.status).to.equal(401);
|
|
136
136
|
}));
|
|
137
137
|
it('403s for a role below MEMBER', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
138
138
|
const app = yield makeApp(eventLogSvc)();
|
|
139
|
-
const res = yield (0,
|
|
139
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
140
140
|
.post('/xenon/api/audit/events')
|
|
141
141
|
.set('x-test-role', 'BOGUS')
|
|
142
142
|
.set('x-test-scopes', 'admin')
|
|
@@ -145,7 +145,7 @@ describe('POST /xenon/api/audit/events — guard (router-level roleGuard(MEMBER)
|
|
|
145
145
|
}));
|
|
146
146
|
it('403s for MEMBER role without the admin scope', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
147
147
|
const app = yield makeApp(eventLogSvc)();
|
|
148
|
-
const res = yield (0,
|
|
148
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
149
149
|
.post('/xenon/api/audit/events')
|
|
150
150
|
.set('x-test-role', 'MEMBER')
|
|
151
151
|
.set('x-test-scopes', 'devices')
|
|
@@ -155,7 +155,7 @@ describe('POST /xenon/api/audit/events — guard (router-level roleGuard(MEMBER)
|
|
|
155
155
|
}));
|
|
156
156
|
it('202s for MEMBER role with the admin scope', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
157
157
|
const app = yield makeApp(eventLogSvc)();
|
|
158
|
-
const res = yield (0,
|
|
158
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
159
159
|
.post('/xenon/api/audit/events')
|
|
160
160
|
.set('x-test-role', 'MEMBER')
|
|
161
161
|
.set('x-test-scopes', 'admin')
|