@xenon-device-management/xenon 2.15.0 → 2.17.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 +38 -26
- package/lib/package.json +2 -1
- package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/index-Dk5J6HB1.js +219 -0
- package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +11 -7
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/app/openapi/sessions.yaml +7 -5
- package/lib/src/app/routers/auth.js +2 -1
- package/lib/src/app/routers/users.js +2 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/event-manager.js +23 -154
- package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-utils.js +25 -8
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +13 -15
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/plugin.js +12 -4
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +29 -33
- package/lib/src/scripts/run-migrations.js +320 -25
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +6 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +6 -2
- package/lib/src/services/SessionLifecycleService.js +52 -23
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +361 -42
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/healing/HealingOrchestrator.js +3 -1
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +23 -4
- package/lib/src/sessions/SessionManager.js +33 -9
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/event-log-session-data.spec.js +11 -4
- package/lib/test/unit/failure-analysis.spec.js +22 -8
- package/lib/test/unit/failure-categories.spec.js +24 -22
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/healing-tier-gate.spec.js +3 -2
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/run-migrations-database.spec.js +440 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +264 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
- package/lib/test/unit/selector-verification-job.spec.js +15 -6
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +43 -96
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +16 -1
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/schema.json +11 -7
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/index-BOxe43p1.js +0 -219
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
|
@@ -13,6 +13,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
const chai_1 = require("chai");
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const os_1 = __importDefault(require("os"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
16
19
|
const sinon_1 = __importDefault(require("sinon"));
|
|
17
20
|
const run_migrations_1 = require("../../src/scripts/run-migrations");
|
|
18
21
|
const config_1 = require("../../src/config");
|
|
@@ -20,6 +23,7 @@ const logger_1 = __importDefault(require("../../src/logger"));
|
|
|
20
23
|
describe('runMigrations', () => {
|
|
21
24
|
afterEach(() => sinon_1.default.restore());
|
|
22
25
|
it('skips with an info log when XENON_AUTO_MIGRATE=false (autoMigrate=false)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
|
+
var _a;
|
|
23
27
|
const orig = config_1.config.autoMigrate;
|
|
24
28
|
config_1.config.autoMigrate = false;
|
|
25
29
|
const info = sinon_1.default.spy(logger_1.default, 'info');
|
|
@@ -29,6 +33,10 @@ describe('runMigrations', () => {
|
|
|
29
33
|
yield (0, run_migrations_1.runMigrations)();
|
|
30
34
|
const calls = info.getCalls().map((c) => c.args.join(' '));
|
|
31
35
|
(0, chai_1.expect)(calls.some((m) => /Auto-migrate disabled/i.test(m))).to.be.true;
|
|
36
|
+
// It says how to choose the command, as the server would.
|
|
37
|
+
const line = (_a = calls.find((m) => /Auto-migrate disabled/i.test(m))) !== null && _a !== void 0 ? _a : '';
|
|
38
|
+
(0, chai_1.expect)(line).to.match(/matches its tables/);
|
|
39
|
+
(0, chai_1.expect)(line).to.match(/P3018/);
|
|
32
40
|
}
|
|
33
41
|
finally {
|
|
34
42
|
config_1.config.autoMigrate = orig;
|
|
@@ -36,3 +44,259 @@ describe('runMigrations', () => {
|
|
|
36
44
|
}
|
|
37
45
|
}));
|
|
38
46
|
});
|
|
47
|
+
describe('planSchemaSync', () => {
|
|
48
|
+
const LOCAL = ['20250101_a', '20250102_b', '20250103_c'];
|
|
49
|
+
const unasked = () => {
|
|
50
|
+
throw new Error('the tables only need comparing when a migration is left to apply');
|
|
51
|
+
};
|
|
52
|
+
it('pushes a database with no migration history, accepting data loss as the default always did', () => {
|
|
53
|
+
const plan = (0, run_migrations_1.planSchemaSync)(null, LOCAL, unasked);
|
|
54
|
+
(0, chai_1.expect)(plan.command).to.equal('db push');
|
|
55
|
+
(0, chai_1.expect)(plan.acceptDataLoss).to.equal(true);
|
|
56
|
+
});
|
|
57
|
+
it('deploys to a database whose history has every migration', () => {
|
|
58
|
+
const history = { applied: [...LOCAL], failed: [] };
|
|
59
|
+
(0, chai_1.expect)((0, run_migrations_1.planSchemaSync)(history, LOCAL, unasked).command).to.equal('migrate deploy');
|
|
60
|
+
});
|
|
61
|
+
it('deploys the missing migrations when the tables are what the history says', () => {
|
|
62
|
+
const history = { applied: LOCAL.slice(0, 2), failed: [] };
|
|
63
|
+
const compared = [];
|
|
64
|
+
const plan = (0, run_migrations_1.planSchemaSync)(history, LOCAL, (applied) => {
|
|
65
|
+
compared.push(applied);
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
(0, chai_1.expect)(plan.command).to.equal('migrate deploy');
|
|
69
|
+
(0, chai_1.expect)(compared).to.deep.equal([LOCAL.slice(0, 2)]);
|
|
70
|
+
});
|
|
71
|
+
it('pushes when the tables are ahead of the history, so deploy would redo a change', () => {
|
|
72
|
+
const history = { applied: LOCAL.slice(0, 1), failed: [] };
|
|
73
|
+
const plan = (0, run_migrations_1.planSchemaSync)(history, LOCAL, () => false);
|
|
74
|
+
(0, chai_1.expect)(plan.command).to.equal('db push');
|
|
75
|
+
(0, chai_1.expect)(plan.acceptDataLoss).to.equal(false);
|
|
76
|
+
(0, chai_1.expect)(plan.reason).to.match(/differ from what its recorded migrations make/);
|
|
77
|
+
// The failure message depends on why: see schemaSyncFailure.
|
|
78
|
+
(0, chai_1.expect)(plan.basis).to.equal('tables-differ');
|
|
79
|
+
});
|
|
80
|
+
it('deploys, as the history says, when the tables could not be compared', () => {
|
|
81
|
+
const history = { applied: LOCAL.slice(0, 1), failed: [] };
|
|
82
|
+
const plan = (0, run_migrations_1.planSchemaSync)(history, LOCAL, () => null);
|
|
83
|
+
(0, chai_1.expect)(plan.command).to.equal('migrate deploy');
|
|
84
|
+
(0, chai_1.expect)(plan.reason).to.match(/could not be compared/);
|
|
85
|
+
});
|
|
86
|
+
it('pushes a database whose history records a failed migration', () => {
|
|
87
|
+
const history = { applied: LOCAL.slice(0, 2), failed: [LOCAL[2]] };
|
|
88
|
+
const plan = (0, run_migrations_1.planSchemaSync)(history, LOCAL, unasked);
|
|
89
|
+
(0, chai_1.expect)(plan.command).to.equal('db push');
|
|
90
|
+
(0, chai_1.expect)(plan.acceptDataLoss).to.equal(false);
|
|
91
|
+
(0, chai_1.expect)(plan.reason).to.include(LOCAL[2]);
|
|
92
|
+
(0, chai_1.expect)(plan.basis).to.equal('failed-migration');
|
|
93
|
+
(0, chai_1.expect)(plan.failed).to.deep.equal([LOCAL[2]]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('runMigrations chooses by the database, not by databaseProvider', () => {
|
|
97
|
+
const scratchUrl = `file:${path_1.default.join(os_1.default.tmpdir(), `xenon-choose-${process.pid}-absent.db`)}`;
|
|
98
|
+
const everyMigration = fs_1.default
|
|
99
|
+
.readdirSync(path_1.default.resolve(__dirname, '../../prisma/migrations'))
|
|
100
|
+
.filter((name) => !name.endsWith('.toml'));
|
|
101
|
+
let saved;
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
saved = {
|
|
104
|
+
autoMigrate: config_1.config.autoMigrate,
|
|
105
|
+
databaseProvider: config_1.config.databaseProvider,
|
|
106
|
+
databaseUrl: config_1.config.databaseUrl,
|
|
107
|
+
};
|
|
108
|
+
config_1.config.autoMigrate = true;
|
|
109
|
+
config_1.config.databaseUrl = scratchUrl;
|
|
110
|
+
});
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
Object.assign(config_1.config, saved);
|
|
113
|
+
});
|
|
114
|
+
/** The prisma subcommand runMigrations ran, given the history the database reports. */
|
|
115
|
+
function commandFor(provider, history) {
|
|
116
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
config_1.config.databaseProvider = provider;
|
|
118
|
+
const commands = [];
|
|
119
|
+
yield (0, run_migrations_1.runMigrations)((_cmd, args) => {
|
|
120
|
+
commands.push(args.filter((a) => !a.startsWith('-') && !a.includes(path_1.default.sep)).join(' '));
|
|
121
|
+
return Buffer.from('ok');
|
|
122
|
+
}, () => __awaiter(this, void 0, void 0, function* () { return history; }));
|
|
123
|
+
return commands[commands.length - 1];
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
for (const provider of ['sqlite', 'postgresql']) {
|
|
127
|
+
it(`pushes a database with no history when databaseProvider is ${provider}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
|
+
(0, chai_1.expect)(yield commandFor(provider, null)).to.equal('db push');
|
|
129
|
+
}));
|
|
130
|
+
it(`deploys to a database with a complete history when databaseProvider is ${provider}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
131
|
+
const history = { applied: everyMigration, failed: [] };
|
|
132
|
+
(0, chai_1.expect)(yield commandFor(provider, history)).to.equal('migrate deploy');
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// `db push --accept-data-loss` drops whatever the schema doesn't have. On a
|
|
137
|
+
// database with no history that is what the default always did. On one with a
|
|
138
|
+
// history it would drop what the history doesn't explain, such as a table added
|
|
139
|
+
// by hand or a half-finished table a failed migration left, where `migrate
|
|
140
|
+
// deploy` (the old `postgresql` path) kept them.
|
|
141
|
+
describe('runMigrations: what each plan runs', () => {
|
|
142
|
+
const scratchUrl = `file:${path_1.default.join(os_1.default.tmpdir(), `xenon-flags-${process.pid}-absent.db`)}`;
|
|
143
|
+
const everyMigration = fs_1.default
|
|
144
|
+
.readdirSync(path_1.default.resolve(__dirname, '../../prisma/migrations'))
|
|
145
|
+
.filter((name) => !name.endsWith('.toml'))
|
|
146
|
+
.sort();
|
|
147
|
+
let saved;
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
saved = { autoMigrate: config_1.config.autoMigrate, databaseUrl: config_1.config.databaseUrl };
|
|
150
|
+
config_1.config.autoMigrate = true;
|
|
151
|
+
config_1.config.databaseUrl = scratchUrl;
|
|
152
|
+
});
|
|
153
|
+
afterEach(() => {
|
|
154
|
+
Object.assign(config_1.config, saved);
|
|
155
|
+
sinon_1.default.restore();
|
|
156
|
+
});
|
|
157
|
+
/** Every prisma call's arguments, with `diff` and the update answered by the given steps. */
|
|
158
|
+
function run(history_1) {
|
|
159
|
+
return __awaiter(this, arguments, void 0, function* (history, steps = {}) {
|
|
160
|
+
const calls = [];
|
|
161
|
+
try {
|
|
162
|
+
yield (0, run_migrations_1.runMigrations)((_cmd, args) => {
|
|
163
|
+
calls.push(args);
|
|
164
|
+
const step = args.includes('diff') ? steps.diff : steps.update;
|
|
165
|
+
return step ? step(args) : Buffer.from('ok');
|
|
166
|
+
}, () => __awaiter(this, void 0, void 0, function* () { return history; }));
|
|
167
|
+
return { calls };
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
return { calls, error };
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const fail = (status, stderr, stdout = '') => () => {
|
|
175
|
+
throw Object.assign(new Error('Command failed'), {
|
|
176
|
+
status,
|
|
177
|
+
stderr: Buffer.from(stderr),
|
|
178
|
+
stdout: Buffer.from(stdout),
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
const update = (calls) => calls[calls.length - 1];
|
|
182
|
+
it('accepts data loss only for a database with no history, as the default always did', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
183
|
+
const { calls } = yield run(null);
|
|
184
|
+
(0, chai_1.expect)(update(calls)).to.include.members(['db', 'push', '--accept-data-loss']);
|
|
185
|
+
}));
|
|
186
|
+
it('pushes without accepting data loss when the tables differ from the history', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
187
|
+
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
188
|
+
const { calls } = yield run(history, { diff: fail(2, '') });
|
|
189
|
+
(0, chai_1.expect)(update(calls)).to.include.members(['db', 'push']);
|
|
190
|
+
(0, chai_1.expect)(update(calls)).to.not.include('--accept-data-loss');
|
|
191
|
+
}));
|
|
192
|
+
it('pushes without accepting data loss when the history records a failed migration', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
193
|
+
const history = { applied: everyMigration.slice(0, -1), failed: everyMigration.slice(-1) };
|
|
194
|
+
const { calls } = yield run(history);
|
|
195
|
+
(0, chai_1.expect)(update(calls)).to.include.members(['db', 'push']);
|
|
196
|
+
(0, chai_1.expect)(update(calls)).to.not.include('--accept-data-loss');
|
|
197
|
+
}));
|
|
198
|
+
it('deploys to a database whose history matches its tables', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
199
|
+
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
200
|
+
const { calls } = yield run(history);
|
|
201
|
+
(0, chai_1.expect)(update(calls)).to.include.members(['migrate', 'deploy']);
|
|
202
|
+
}));
|
|
203
|
+
// What `db push` without --accept-data-loss prints when it would drop a table
|
|
204
|
+
// that holds rows: the list on stdout, the refusal on stderr.
|
|
205
|
+
const refusesDataLoss = fail(1, 'Error: Use the --accept-data-loss flag to ignore the data loss warnings like prisma db push --accept-data-loss', '⚠️ There might be data loss when applying the changes:\n\n' +
|
|
206
|
+
' • You are about to drop the `LabNotes` table, which is not empty (1 rows).');
|
|
207
|
+
const root = path_1.default.resolve(__dirname, '../..');
|
|
208
|
+
const prisma = path_1.default.join(root, 'node_modules', '.bin', 'prisma');
|
|
209
|
+
const schema = path_1.default.join(root, 'prisma', 'schema.prisma');
|
|
210
|
+
const scratchFile = scratchUrl.slice('file:'.length);
|
|
211
|
+
/** The commands a failure message gives, one to an indented line, in its order. */
|
|
212
|
+
const commandsIn = (message) => message
|
|
213
|
+
.split('\n')
|
|
214
|
+
.filter((line) => /^ {2}(DATABASE_URL=|echo )/.test(line))
|
|
215
|
+
.map((line) => line.trim());
|
|
216
|
+
const letGo = `DATABASE_URL=file:${scratchFile} ${prisma} db push --skip-generate --accept-data-loss ` +
|
|
217
|
+
`--schema ${schema}`;
|
|
218
|
+
// A history database whose tables differ from its migrations: `migrate
|
|
219
|
+
// deploy` works on it when something was added by hand, and fails (P3018)
|
|
220
|
+
// when db push moved the tables past the history. The message can't tell
|
|
221
|
+
// which, so it never sends migrate deploy to the file before a copy.
|
|
222
|
+
it('says how to let go of what db push would delete, or keep it by trying migrate deploy on a copy first', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
223
|
+
var _a;
|
|
224
|
+
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
225
|
+
const { error } = yield run(history, { diff: fail(2, ''), update: refusesDataLoss });
|
|
226
|
+
(0, chai_1.expect)(error, 'the start stops').to.be.an('Error');
|
|
227
|
+
const message = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '';
|
|
228
|
+
(0, chai_1.expect)(message).to.match(/stopped rather than delete/);
|
|
229
|
+
(0, chai_1.expect)(message).to.match(/differ from what its recorded migrations make/);
|
|
230
|
+
// Prisma's own list of what it would drop, which is only on stdout.
|
|
231
|
+
(0, chai_1.expect)(message).to.include('LabNotes');
|
|
232
|
+
(0, chai_1.expect)(message).to.not.match(/required column/);
|
|
233
|
+
(0, chai_1.expect)(message, 'no plain advice to migrate deploy the file').to.not.match(/by hand with prisma migrate deploy/);
|
|
234
|
+
const commands = commandsIn(message);
|
|
235
|
+
const backup = commands.findIndex((c) => c.includes(`VACUUM INTO '${scratchFile}.backup-`) &&
|
|
236
|
+
c.endsWith(`| ${prisma} db execute --url file:${scratchFile} --stdin`));
|
|
237
|
+
(0, chai_1.expect)(backup, `a backup first:\n${message}`).to.equal(0);
|
|
238
|
+
(0, chai_1.expect)(commands).to.include(letGo);
|
|
239
|
+
const onCopy = commands.findIndex((c) => c.startsWith(`DATABASE_URL=file:${scratchFile}.copy ${prisma} migrate deploy --schema ${schema}`));
|
|
240
|
+
const copied = commands.findIndex((c) => c.includes(`VACUUM INTO '${scratchFile}.copy'`));
|
|
241
|
+
const onFile = commands.indexOf(`DATABASE_URL=file:${scratchFile} ${prisma} migrate deploy --schema ${schema}`);
|
|
242
|
+
(0, chai_1.expect)(copied, 'the copy is made').to.be.greaterThan(backup);
|
|
243
|
+
(0, chai_1.expect)(onCopy, 'migrate deploy runs on the copy').to.be.greaterThan(copied);
|
|
244
|
+
(0, chai_1.expect)(onFile, 'and on the file only after the copy').to.be.greaterThan(onCopy);
|
|
245
|
+
}));
|
|
246
|
+
// With a failed migration recorded, `migrate deploy` stops at once (P3009),
|
|
247
|
+
// whatever the tables hold, so the message never suggests it.
|
|
248
|
+
it('names the failed migration and never suggests migrate deploy when the history records one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
249
|
+
var _a;
|
|
250
|
+
const failed = everyMigration[everyMigration.length - 1];
|
|
251
|
+
const history = { applied: everyMigration.slice(0, -1), failed: [failed] };
|
|
252
|
+
const { error } = yield run(history, { update: refusesDataLoss });
|
|
253
|
+
(0, chai_1.expect)(error, 'the start stops').to.be.an('Error');
|
|
254
|
+
const message = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '';
|
|
255
|
+
(0, chai_1.expect)(message).to.match(/stopped rather than delete/);
|
|
256
|
+
(0, chai_1.expect)(message).to.include(failed);
|
|
257
|
+
(0, chai_1.expect)(message).to.match(/records a failed migration/);
|
|
258
|
+
(0, chai_1.expect)(message).to.not.match(/differ from what its recorded migrations make/);
|
|
259
|
+
(0, chai_1.expect)(message).to.include('LabNotes');
|
|
260
|
+
const commands = commandsIn(message);
|
|
261
|
+
(0, chai_1.expect)(commands[0]).to.include(`VACUUM INTO '${scratchFile}.backup-`);
|
|
262
|
+
(0, chai_1.expect)(commands).to.include(letGo);
|
|
263
|
+
(0, chai_1.expect)(commands.filter((c) => c.includes('migrate deploy'))).to.deep.equal([]);
|
|
264
|
+
(0, chai_1.expect)(message).to.not.match(/by hand with prisma migrate deploy/);
|
|
265
|
+
}));
|
|
266
|
+
// A migration can fail on what is already in the tables, which a later
|
|
267
|
+
// start deals with, or on the rows (a unique index over duplicate values),
|
|
268
|
+
// which no start gets past until someone changes them.
|
|
269
|
+
it('says what to check when migrate deploy fails on a migration (P3018), without promising the next start recovers', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
270
|
+
var _a;
|
|
271
|
+
const history = { applied: everyMigration.slice(0, -1), failed: [] };
|
|
272
|
+
const { error } = yield run(history, {
|
|
273
|
+
diff: fail(1, 'simulated diff failure'),
|
|
274
|
+
update: fail(1, 'Error: P3018\n\nA migration failed to apply.\n\nMigration name: 20261101000000_uniq\n\n' +
|
|
275
|
+
'Database error:\nUNIQUE constraint failed: LocatorEtalon.strategy, LocatorEtalon.nodeName'),
|
|
276
|
+
});
|
|
277
|
+
(0, chai_1.expect)(error, 'the start stops').to.be.an('Error');
|
|
278
|
+
const message = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '';
|
|
279
|
+
(0, chai_1.expect)(message).to.include('20261101000000_uniq');
|
|
280
|
+
(0, chai_1.expect)(message).to.include('UNIQUE constraint failed');
|
|
281
|
+
(0, chai_1.expect)(message).to.not.match(/next start brings the database up to date/);
|
|
282
|
+
(0, chai_1.expect)(message).to.not.match(/start again, or first/);
|
|
283
|
+
// Both things to check: a change already there, and rows the migration can't take.
|
|
284
|
+
(0, chai_1.expect)(message).to.match(/already there/);
|
|
285
|
+
(0, chai_1.expect)(message).to.match(/duplicate values/);
|
|
286
|
+
(0, chai_1.expect)(message).to.match(/change those rows/);
|
|
287
|
+
}));
|
|
288
|
+
it("says it couldn't read the history without blaming the file outright", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
289
|
+
let error;
|
|
290
|
+
try {
|
|
291
|
+
yield (0, run_migrations_1.runMigrations)(() => Buffer.from('ok'), () => __awaiter(void 0, void 0, void 0, function* () {
|
|
292
|
+
throw new Error('database is locked');
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
error = e;
|
|
297
|
+
}
|
|
298
|
+
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.match(/could not read the migration history/);
|
|
299
|
+
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.match(/another server/);
|
|
300
|
+
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.include('database is locked');
|
|
301
|
+
}));
|
|
302
|
+
});
|
|
@@ -25,7 +25,9 @@ describe('POST /healing/selector/state (real queries)', function () {
|
|
|
25
25
|
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
26
26
|
yield (0, selector_health_fixture_1.seedSelectorHealth)(scratch.db);
|
|
27
27
|
// The service's transactions use the scratch database's own client.
|
|
28
|
-
typedi_1.Container.set(SelectorStateService_1.SelectorStateService, new SelectorStateService_1.SelectorStateService(scratch.db, {
|
|
28
|
+
typedi_1.Container.set(SelectorStateService_1.SelectorStateService, new SelectorStateService_1.SelectorStateService(scratch.db, {
|
|
29
|
+
emitToDashboardForSelector: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
|
|
30
|
+
}));
|
|
29
31
|
}));
|
|
30
32
|
after(() => {
|
|
31
33
|
typedi_1.Container.remove(SelectorStateService_1.SelectorStateService);
|
|
@@ -38,7 +38,7 @@ function buildPrismaStub() {
|
|
|
38
38
|
}
|
|
39
39
|
function buildSocketStub() {
|
|
40
40
|
return {
|
|
41
|
-
|
|
41
|
+
emitToDashboardForSelector: sinon_1.default.stub().resolves(),
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
const STRATEGY = 'accessibility id';
|
|
@@ -105,8 +105,8 @@ describe('SelectorStateService', () => {
|
|
|
105
105
|
selector: SELECTOR,
|
|
106
106
|
apiKeyId: API_KEY_ID,
|
|
107
107
|
});
|
|
108
|
-
(0, chai_1.expect)(socketStub.
|
|
109
|
-
const [event, payload] = socketStub.
|
|
108
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.equal(true);
|
|
109
|
+
const [event, payload] = socketStub.emitToDashboardForSelector.firstCall.args;
|
|
110
110
|
(0, chai_1.expect)(event).to.equal(SocketEvents_1.SocketEvents.SELECTOR_FIXED);
|
|
111
111
|
(0, chai_1.expect)(payload).to.include({
|
|
112
112
|
original_strategy: STRATEGY,
|
|
@@ -133,7 +133,7 @@ describe('SelectorStateService', () => {
|
|
|
133
133
|
(0, chai_1.expect)(err).to.be.instanceOf(SelectorStateService_1.SelectorStateConflictError);
|
|
134
134
|
(0, chai_1.expect)(err.currentStatus).to.equal('muted');
|
|
135
135
|
(0, chai_1.expect)(prismaStub.selectorState.upsert.called).to.equal(false);
|
|
136
|
-
(0, chai_1.expect)(socketStub.
|
|
136
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
|
|
137
137
|
}));
|
|
138
138
|
});
|
|
139
139
|
describe('mute', () => {
|
|
@@ -170,8 +170,8 @@ describe('SelectorStateService', () => {
|
|
|
170
170
|
selector: SELECTOR,
|
|
171
171
|
apiKeyId: API_KEY_ID,
|
|
172
172
|
});
|
|
173
|
-
(0, chai_1.expect)(socketStub.
|
|
174
|
-
const [event, payload] = socketStub.
|
|
173
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.equal(true);
|
|
174
|
+
const [event, payload] = socketStub.emitToDashboardForSelector.firstCall.args;
|
|
175
175
|
(0, chai_1.expect)(event).to.equal(SocketEvents_1.SocketEvents.SELECTOR_MUTED);
|
|
176
176
|
(0, chai_1.expect)(payload.status).to.equal('muted');
|
|
177
177
|
(0, chai_1.expect)(payload.muted_at).to.equal('2026-04-25T02:00:00.000Z');
|
|
@@ -195,8 +195,8 @@ describe('SelectorStateService', () => {
|
|
|
195
195
|
});
|
|
196
196
|
(0, chai_1.expect)(prismaStub.selectorState.delete.calledOnce).to.equal(true);
|
|
197
197
|
(0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
|
|
198
|
-
(0, chai_1.expect)(socketStub.
|
|
199
|
-
(0, chai_1.expect)(socketStub.
|
|
198
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.equal(true);
|
|
199
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_UNMUTED);
|
|
200
200
|
}));
|
|
201
201
|
it('keeps the row and sets status=active when history exists', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
202
202
|
const muted = makeRow({
|
|
@@ -217,8 +217,8 @@ describe('SelectorStateService', () => {
|
|
|
217
217
|
const args = prismaStub.selectorState.update.firstCall.args[0];
|
|
218
218
|
(0, chai_1.expect)(args.data.status).to.equal('active');
|
|
219
219
|
(0, chai_1.expect)(args.data.muted_at).to.equal(null);
|
|
220
|
-
(0, chai_1.expect)(socketStub.
|
|
221
|
-
(0, chai_1.expect)(socketStub.
|
|
220
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.equal(true);
|
|
221
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_UNMUTED);
|
|
222
222
|
}));
|
|
223
223
|
it('is a no-op when the row exists but is not muted', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
224
224
|
prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'active' }));
|
|
@@ -229,7 +229,7 @@ describe('SelectorStateService', () => {
|
|
|
229
229
|
});
|
|
230
230
|
(0, chai_1.expect)(prismaStub.selectorState.delete.called).to.equal(false);
|
|
231
231
|
(0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
|
|
232
|
-
(0, chai_1.expect)(socketStub.
|
|
232
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
|
|
233
233
|
}));
|
|
234
234
|
it('is a no-op when no row exists', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
235
235
|
prismaStub.selectorState.findUnique.resolves(null);
|
|
@@ -240,7 +240,7 @@ describe('SelectorStateService', () => {
|
|
|
240
240
|
});
|
|
241
241
|
(0, chai_1.expect)(prismaStub.selectorState.delete.called).to.equal(false);
|
|
242
242
|
(0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
|
|
243
|
-
(0, chai_1.expect)(socketStub.
|
|
243
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
|
|
244
244
|
}));
|
|
245
245
|
});
|
|
246
246
|
describe('cancelVerification', () => {
|
|
@@ -261,7 +261,7 @@ describe('SelectorStateService', () => {
|
|
|
261
261
|
(0, chai_1.expect)(err.currentStatus).to.equal('active');
|
|
262
262
|
(0, chai_1.expect)(prismaStub.selectorState.delete.called).to.equal(false);
|
|
263
263
|
(0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
|
|
264
|
-
(0, chai_1.expect)(socketStub.
|
|
264
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
|
|
265
265
|
}));
|
|
266
266
|
it('throws SelectorStateConflictError when no row exists', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
267
267
|
prismaStub.selectorState.findUnique.resolves(null);
|
|
@@ -294,8 +294,8 @@ describe('SelectorStateService', () => {
|
|
|
294
294
|
});
|
|
295
295
|
(0, chai_1.expect)(prismaStub.selectorState.delete.calledOnce).to.equal(true);
|
|
296
296
|
(0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
|
|
297
|
-
(0, chai_1.expect)(socketStub.
|
|
298
|
-
(0, chai_1.expect)(socketStub.
|
|
297
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.equal(true);
|
|
298
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_CANCELLED);
|
|
299
299
|
}));
|
|
300
300
|
it('keeps row and clears fixed state when history exists (resolved_at not null)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
301
301
|
const pending = makeRow({
|
|
@@ -318,7 +318,7 @@ describe('SelectorStateService', () => {
|
|
|
318
318
|
(0, chai_1.expect)(args.data.status).to.equal('active');
|
|
319
319
|
(0, chai_1.expect)(args.data.fixed_at).to.equal(null);
|
|
320
320
|
(0, chai_1.expect)(args.data.clean_builds_count).to.equal(0);
|
|
321
|
-
(0, chai_1.expect)(socketStub.
|
|
321
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_CANCELLED);
|
|
322
322
|
}));
|
|
323
323
|
});
|
|
324
324
|
describe('onHealRecorded', () => {
|
|
@@ -344,8 +344,8 @@ describe('SelectorStateService', () => {
|
|
|
344
344
|
(0, chai_1.expect)(args.data.resolved_at).to.equal(null);
|
|
345
345
|
(0, chai_1.expect)(args.data.regression_count).to.deep.equal({ increment: 1 });
|
|
346
346
|
(0, chai_1.expect)(args.data.clean_builds_count).to.equal(0);
|
|
347
|
-
(0, chai_1.expect)(socketStub.
|
|
348
|
-
(0, chai_1.expect)(socketStub.
|
|
347
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.equal(true);
|
|
348
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED);
|
|
349
349
|
}));
|
|
350
350
|
it('flips resolved → active, clears resolved_at, and increments regression_count', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
351
351
|
const resolved = makeRow({
|
|
@@ -368,7 +368,7 @@ describe('SelectorStateService', () => {
|
|
|
368
368
|
(0, chai_1.expect)(args.data.resolved_at).to.equal(null);
|
|
369
369
|
(0, chai_1.expect)(args.data.fixed_at).to.equal(null);
|
|
370
370
|
(0, chai_1.expect)(args.data.regression_count).to.deep.equal({ increment: 1 });
|
|
371
|
-
(0, chai_1.expect)(socketStub.
|
|
371
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED);
|
|
372
372
|
}));
|
|
373
373
|
it('is a no-op when the row is muted', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
374
374
|
prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'muted' }));
|
|
@@ -378,7 +378,7 @@ describe('SelectorStateService', () => {
|
|
|
378
378
|
sessionId: SESSION_ID,
|
|
379
379
|
});
|
|
380
380
|
(0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
|
|
381
|
-
(0, chai_1.expect)(socketStub.
|
|
381
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
|
|
382
382
|
}));
|
|
383
383
|
it('is a no-op when no row exists', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
384
384
|
prismaStub.selectorState.findUnique.resolves(null);
|
|
@@ -388,7 +388,7 @@ describe('SelectorStateService', () => {
|
|
|
388
388
|
sessionId: SESSION_ID,
|
|
389
389
|
});
|
|
390
390
|
(0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
|
|
391
|
-
(0, chai_1.expect)(socketStub.
|
|
391
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
|
|
392
392
|
}));
|
|
393
393
|
it('is a no-op when row is already active', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
394
394
|
prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'active' }));
|
|
@@ -398,7 +398,7 @@ describe('SelectorStateService', () => {
|
|
|
398
398
|
sessionId: SESSION_ID,
|
|
399
399
|
});
|
|
400
400
|
(0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
|
|
401
|
-
(0, chai_1.expect)(socketStub.
|
|
401
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
|
|
402
402
|
}));
|
|
403
403
|
});
|
|
404
404
|
describe('getState', () => {
|
|
@@ -443,7 +443,7 @@ describe('SelectorStateService', () => {
|
|
|
443
443
|
thrown = e;
|
|
444
444
|
}
|
|
445
445
|
(0, chai_1.expect)(thrown.message).to.equal('disk full');
|
|
446
|
-
(0, chai_1.expect)(socketStub.
|
|
446
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
|
|
447
447
|
}));
|
|
448
448
|
it('records the mute with its reason', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
449
449
|
prismaStub.selectorState.upsert.resolves(makeRow({ status: 'muted', muted_at: new Date() }));
|
|
@@ -483,4 +483,53 @@ describe('SelectorStateService', () => {
|
|
|
483
483
|
(0, chai_1.expect)(prismaStub.selectorEvent.create.called).to.equal(false);
|
|
484
484
|
}));
|
|
485
485
|
});
|
|
486
|
+
// The socket server sends a selector's event only to the dashboards whose
|
|
487
|
+
// caller may see the selector, so every event names it.
|
|
488
|
+
describe('each event names its selector, for the socket server to scope it', () => {
|
|
489
|
+
const KEY = { strategy: STRATEGY, selector: SELECTOR };
|
|
490
|
+
const ctx = { strategy: STRATEGY, selector: SELECTOR, apiKeyId: API_KEY_ID };
|
|
491
|
+
const sent = () => socketStub.emitToDashboardForSelector.getCalls().map((c) => [c.args[0], c.args[2]]);
|
|
492
|
+
it('markFixed and mute', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
493
|
+
prismaStub.selectorState.findUnique.resolves(null);
|
|
494
|
+
prismaStub.selectorState.upsert.resolves(makeRow({ status: 'pending' }));
|
|
495
|
+
yield svc.markFixed(ctx);
|
|
496
|
+
prismaStub.selectorState.upsert.resolves(makeRow({ status: 'muted' }));
|
|
497
|
+
yield svc.mute(ctx);
|
|
498
|
+
(0, chai_1.expect)(sent()).to.deep.equal([
|
|
499
|
+
[SocketEvents_1.SocketEvents.SELECTOR_FIXED, KEY],
|
|
500
|
+
[SocketEvents_1.SocketEvents.SELECTOR_MUTED, KEY],
|
|
501
|
+
]);
|
|
502
|
+
}));
|
|
503
|
+
it('unmute and cancelVerification, whether the row is kept or deleted', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
504
|
+
prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'muted' }));
|
|
505
|
+
yield svc.unmute(ctx);
|
|
506
|
+
prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'muted', regression_count: 1 }));
|
|
507
|
+
prismaStub.selectorState.update.resolves(makeRow({ status: 'active', regression_count: 1 }));
|
|
508
|
+
yield svc.unmute(ctx);
|
|
509
|
+
prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'pending' }));
|
|
510
|
+
yield svc.cancelVerification(ctx);
|
|
511
|
+
prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'pending', regression_count: 1 }));
|
|
512
|
+
yield svc.cancelVerification(ctx);
|
|
513
|
+
(0, chai_1.expect)(sent()).to.deep.equal([
|
|
514
|
+
[SocketEvents_1.SocketEvents.SELECTOR_UNMUTED, KEY],
|
|
515
|
+
[SocketEvents_1.SocketEvents.SELECTOR_UNMUTED, KEY],
|
|
516
|
+
[SocketEvents_1.SocketEvents.SELECTOR_CANCELLED, KEY],
|
|
517
|
+
[SocketEvents_1.SocketEvents.SELECTOR_CANCELLED, KEY],
|
|
518
|
+
]);
|
|
519
|
+
}));
|
|
520
|
+
it('onHealRecorded', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
521
|
+
prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'pending' }));
|
|
522
|
+
prismaStub.selectorState.update.resolves(makeRow({ status: 'active', regression_count: 1 }));
|
|
523
|
+
yield svc.onHealRecorded({ strategy: STRATEGY, selector: SELECTOR, sessionId: SESSION_ID });
|
|
524
|
+
(0, chai_1.expect)(sent()).to.deep.equal([[SocketEvents_1.SocketEvents.SELECTOR_REGRESSED, KEY]]);
|
|
525
|
+
}));
|
|
526
|
+
it('a heal recorded with no strategy names strategy ""', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
527
|
+
prismaStub.selectorState.findUnique.resolves(null);
|
|
528
|
+
prismaStub.selectorState.upsert.resolves(makeRow({ status: 'muted', original_strategy: '' }));
|
|
529
|
+
yield svc.mute({ strategy: '', selector: SELECTOR, apiKeyId: API_KEY_ID });
|
|
530
|
+
(0, chai_1.expect)(sent()).to.deep.equal([
|
|
531
|
+
[SocketEvents_1.SocketEvents.SELECTOR_MUTED, { strategy: '', selector: SELECTOR }],
|
|
532
|
+
]);
|
|
533
|
+
}));
|
|
534
|
+
});
|
|
486
535
|
});
|
|
@@ -59,7 +59,7 @@ describe('SelectorVerificationJob counts only builds where the selector was foun
|
|
|
59
59
|
yield scratch.db.selectorEvent.deleteMany();
|
|
60
60
|
yield scratch.db.selectorState.deleteMany();
|
|
61
61
|
yield scratch.db.sessionLog.deleteMany();
|
|
62
|
-
socket = {
|
|
62
|
+
socket = { emitToDashboardForSelector: sinon_1.default.stub().resolves() };
|
|
63
63
|
yield scratch.db.selectorState.create({
|
|
64
64
|
data: {
|
|
65
65
|
original_strategy: 'xpath',
|
|
@@ -49,7 +49,7 @@ describe('SelectorVerificationJob.run', () => {
|
|
|
49
49
|
prismaStub.$transaction = sinon_1.default
|
|
50
50
|
.stub()
|
|
51
51
|
.callsFake((fn) => fn(prismaStub));
|
|
52
|
-
socketStub = {
|
|
52
|
+
socketStub = { emitToDashboardForSelector: sinon_1.default.stub().resolves() };
|
|
53
53
|
job = new SelectorVerificationJob_1.SelectorVerificationJob(prismaStub, socketStub);
|
|
54
54
|
});
|
|
55
55
|
afterEach(() => sinon_1.default.restore());
|
|
@@ -84,8 +84,13 @@ describe('SelectorVerificationJob.run', () => {
|
|
|
84
84
|
(0, chai_1.expect)(updateArg.data.status).to.equal('resolved');
|
|
85
85
|
(0, chai_1.expect)(updateArg.data.resolved_at).to.be.instanceOf(Date);
|
|
86
86
|
(0, chai_1.expect)(updateArg.data.clean_builds_count).to.equal(3);
|
|
87
|
-
(0, chai_1.expect)(socketStub.
|
|
88
|
-
(0, chai_1.expect)(socketStub.
|
|
87
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.be.true;
|
|
88
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal('selector_resolved');
|
|
89
|
+
// Named, so only the dashboards that may see the selector get it.
|
|
90
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[2]).to.deep.equal({
|
|
91
|
+
strategy: 'xpath',
|
|
92
|
+
selector: '//x',
|
|
93
|
+
});
|
|
89
94
|
}));
|
|
90
95
|
it('updates clean_builds_count and emits PROGRESS when below 3 and value changed', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
96
|
prismaStub.selectorState.findMany.resolves([
|
|
@@ -111,7 +116,11 @@ describe('SelectorVerificationJob.run', () => {
|
|
|
111
116
|
const updateArg = prismaStub.selectorState.update.firstCall.args[0];
|
|
112
117
|
(0, chai_1.expect)(updateArg.data.status).to.be.undefined;
|
|
113
118
|
(0, chai_1.expect)(updateArg.data.clean_builds_count).to.equal(1);
|
|
114
|
-
(0, chai_1.expect)(socketStub.
|
|
119
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal('selector_progress');
|
|
120
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[2]).to.deep.equal({
|
|
121
|
+
strategy: 'xpath',
|
|
122
|
+
selector: '//x',
|
|
123
|
+
});
|
|
115
124
|
}));
|
|
116
125
|
it('does not write or emit when clean_builds_count is unchanged and not promoting', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
126
|
prismaStub.selectorState.findMany.resolves([
|
|
@@ -130,7 +139,7 @@ describe('SelectorVerificationJob.run', () => {
|
|
|
130
139
|
]);
|
|
131
140
|
yield job.run();
|
|
132
141
|
(0, chai_1.expect)(prismaStub.selectorState.update.called).to.be.false;
|
|
133
|
-
(0, chai_1.expect)(socketStub.
|
|
142
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.be.false;
|
|
134
143
|
}));
|
|
135
144
|
it('SQL filters out builds with NULL build_id and only counts findElement(s) commands', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
136
145
|
prismaStub.selectorState.findMany.resolves([
|
|
@@ -160,7 +169,7 @@ describe('SelectorVerificationJob.run', () => {
|
|
|
160
169
|
yield job.run();
|
|
161
170
|
(0, chai_1.expect)(prismaStub.$queryRaw.called).to.be.false;
|
|
162
171
|
(0, chai_1.expect)(prismaStub.selectorState.update.called).to.be.false;
|
|
163
|
-
(0, chai_1.expect)(socketStub.
|
|
172
|
+
(0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.be.false;
|
|
164
173
|
}));
|
|
165
174
|
it('continues processing remaining rows if one fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
166
175
|
prismaStub.selectorState.findMany.resolves([
|
|
@@ -53,7 +53,7 @@ describe('SelectorVerificationJob catches a heal the live check missed', functio
|
|
|
53
53
|
yield scratch.db.selectorEvent.deleteMany();
|
|
54
54
|
yield scratch.db.selectorState.deleteMany();
|
|
55
55
|
yield scratch.db.sessionLog.deleteMany();
|
|
56
|
-
socket = {
|
|
56
|
+
socket = { emitToDashboardForSelector: sinon_1.default.stub().resolves() };
|
|
57
57
|
}));
|
|
58
58
|
const find = (build, selector, healed, at) => scratch.db.sessionLog.create({
|
|
59
59
|
data: {
|
|
@@ -105,7 +105,7 @@ describe('SelectorVerificationJob catches a heal the live check missed', functio
|
|
|
105
105
|
(0, chai_1.expect)(row).to.include({ status: 'active', regression_count: 1, clean_builds_count: 0 });
|
|
106
106
|
(0, chai_1.expect)(row.fixed_at).to.equal(null);
|
|
107
107
|
(0, chai_1.expect)(yield actionsOf('//pending/broke')).to.deep.equal(['broke_again']);
|
|
108
|
-
(0, chai_1.expect)(socket.
|
|
108
|
+
(0, chai_1.expect)(socket.emitToDashboardForSelector.firstCall.args[0]).to.equal('selector_regressed');
|
|
109
109
|
}));
|
|
110
110
|
it('sends a fixed selector back to fix', () => __awaiter(this, void 0, void 0, function* () {
|
|
111
111
|
yield state('//resolved/broke', 'resolved');
|