@xenon-device-management/xenon 1.11.2 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenon-device-management/xenon",
3
- "version": "1.11.2",
3
+ "version": "1.12.1",
4
4
  "description": "Xenon - Intelligent Mobile Infrastructure. A self-healing device orchestration platform for Appium.",
5
5
  "main": "./lib/src/index.js",
6
6
  "exports": {
package/lib/schema.json CHANGED
@@ -224,6 +224,21 @@
224
224
  "default": true,
225
225
  "description": "When true, the cleanup job also deletes session video recordings and screenshots from disk (not just DB rows)."
226
226
  },
227
+ "recordingCleanupDays": {
228
+ "type": "number",
229
+ "default": 30,
230
+ "description": "Live Devices recordings older than this many days are purged (DB row + files) by the cleanup job. Runs on `buildCleanupSchedule`."
231
+ },
232
+ "recordingCleanupMaxCount": {
233
+ "type": "number",
234
+ "default": 100,
235
+ "description": "Maximum number of recordings to retain. Oldest-first eviction beyond this cap regardless of `recordingCleanupDays`."
236
+ },
237
+ "recordingFailedCleanupDays": {
238
+ "type": "number",
239
+ "default": 2,
240
+ "description": "Failed recordings hold no playable file, so they are purged on this much shorter window than `recordingCleanupDays`."
241
+ },
227
242
  "sessionHeartbeatIntervalMs": {
228
243
  "type": "number",
229
244
  "default": 30000,
@@ -46,6 +46,9 @@ exports.DefaultPluginArgs = {
46
46
  buildCleanupMaxCount: 100,
47
47
  buildCleanupSchedule: '0 0 * * *',
48
48
  deleteBuildAssets: true,
49
+ recordingCleanupDays: 30,
50
+ recordingCleanupMaxCount: 100,
51
+ recordingFailedCleanupDays: 2,
49
52
  sessionHeartbeatIntervalMs: 30000,
50
53
  enableJsonLogging: false,
51
54
  autowait: {
@@ -25,17 +25,22 @@ const logger_1 = __importDefault(require("../logger"));
25
25
  const fs_1 = __importDefault(require("fs"));
26
26
  const path_1 = __importDefault(require("path"));
27
27
  const config_1 = require("../config");
28
+ const recordingCleanup_1 = require("./recording/recordingCleanup");
29
+ /** Subdirectory holding composite (multi-device) outputs, keyed by group id. */
30
+ const GROUPS_DIR = '_groups';
28
31
  let CleanupService = class CleanupService {
29
32
  constructor() {
30
33
  this.log = logger_1.default.scope('CleanupService');
31
34
  }
32
35
  /**
33
- * Orchestrates build + session cleanup based on retention policy.
36
+ * Orchestrates build + session + recording cleanup based on retention policy.
34
37
  *
35
- * Two phases:
38
+ * Phases:
36
39
  * 1. Purge old builds (by age AND count cap) together with their sessions.
37
40
  * 2. Sweep orphan sessions (no build_id) older than the retention window —
38
41
  * otherwise ad-hoc sessions accumulate forever.
42
+ * 3. Purge expired Live Devices recordings and sweep unreachable recording
43
+ * directories (issue #209) — the one asset class nothing used to remove.
39
44
  */
40
45
  runCleanup(pluginArgs) {
41
46
  return __awaiter(this, void 0, void 0, function* () {
@@ -84,6 +89,9 @@ let CleanupService = class CleanupService {
84
89
  // this, ad-hoc sessions (WebDriver runs with no build capability) are
85
90
  // never cleaned up — their assets accumulate on disk forever.
86
91
  yield this.purgeOrphanSessions(expirationDate, deleteBuildAssets);
92
+ // 4. Recordings have their own windows — a failed recording holds no
93
+ // playable file, so it need not linger as long as real footage.
94
+ yield this.purgeRecordings(pluginArgs);
87
95
  this.log.info('✅ Build cleanup completed successfully.');
88
96
  }
89
97
  catch (err) {
@@ -91,6 +99,119 @@ let CleanupService = class CleanupService {
91
99
  }
92
100
  });
93
101
  }
102
+ /**
103
+ * Retention for Live Devices recordings (issue #209).
104
+ *
105
+ * Two halves, in order — expiring rows first means their directories are
106
+ * unreachable by the time the sweep runs, so both are reclaimed in one pass:
107
+ * a. delete rows past the age/count policy, and their files;
108
+ * b. remove directories no surviving row points into.
109
+ *
110
+ * Bookmarks and annotations cascade on the row delete, so no child cleanup
111
+ * is needed here.
112
+ */
113
+ purgeRecordings(pluginArgs) {
114
+ return __awaiter(this, void 0, void 0, function* () {
115
+ const { recordingCleanupDays = 30, recordingCleanupMaxCount = 100, recordingFailedCleanupDays = 2, } = pluginArgs;
116
+ const rows = yield prisma_1.prisma.recording.findMany({
117
+ select: { id: true, status: true, started_at: true, file_path: true, group_id: true },
118
+ });
119
+ const expiredIds = (0, recordingCleanup_1.selectExpiredRecordings)({
120
+ rows,
121
+ now: Date.now(),
122
+ days: recordingCleanupDays,
123
+ maxCount: recordingCleanupMaxCount,
124
+ failedDays: recordingFailedCleanupDays,
125
+ });
126
+ if (expiredIds.length > 0) {
127
+ const expired = new Set(expiredIds);
128
+ for (const r of rows) {
129
+ if (expired.has(r.id) && r.file_path)
130
+ this.unlinkSilently(r.file_path);
131
+ }
132
+ yield prisma_1.prisma.recording.deleteMany({ where: { id: { in: expiredIds } } });
133
+ this.log.info(`Purged ${expiredIds.length} expired recording(s) ` +
134
+ `(retention ${recordingCleanupDays}d / max ${recordingCleanupMaxCount}, ` +
135
+ `failed ${recordingFailedCleanupDays}d).`);
136
+ }
137
+ else {
138
+ this.log.info('No recordings past their retention window.');
139
+ }
140
+ this.sweepOrphanRecordingDirs(rows.filter((r) => !expiredIds.includes(r.id)), rows.length);
141
+ });
142
+ }
143
+ /**
144
+ * Remove recording directories nothing in the DB can reach.
145
+ *
146
+ * Reachability is decided by `file_path`, never by the directory name — see
147
+ * selectOrphanDirectories for why the name is not a safe key.
148
+ */
149
+ sweepOrphanRecordingDirs(survivors, totalRowsRead) {
150
+ const base = config_1.config.recordingsAssetsPath;
151
+ let deviceDirs = [];
152
+ let groupDirs = [];
153
+ try {
154
+ if (!fs_1.default.existsSync(base))
155
+ return;
156
+ deviceDirs = fs_1.default
157
+ .readdirSync(base, { withFileTypes: true })
158
+ .filter((e) => e.isDirectory() && e.name !== GROUPS_DIR)
159
+ .map((e) => path_1.default.join(base, e.name));
160
+ const groupsBase = path_1.default.join(base, GROUPS_DIR);
161
+ if (fs_1.default.existsSync(groupsBase)) {
162
+ groupDirs = fs_1.default
163
+ .readdirSync(groupsBase, { withFileTypes: true })
164
+ .filter((e) => e.isDirectory())
165
+ .map((e) => path_1.default.join(groupsBase, e.name));
166
+ }
167
+ }
168
+ catch (err) {
169
+ this.log.warn(`Could not list recording directories: ${err.message}`);
170
+ return;
171
+ }
172
+ if (!(0, recordingCleanup_1.isRecordingSweepSafe)({
173
+ rowCount: totalRowsRead,
174
+ dirCount: deviceDirs.length + groupDirs.length,
175
+ })) {
176
+ this.log.warn(`Refusing to sweep ${deviceDirs.length + groupDirs.length} recording directories: the ` +
177
+ 'Recording table read returned no rows. That is what a failed or mis-scoped query ' +
178
+ 'looks like, and sweeping on it would delete the whole tree. Sweep manually if the ' +
179
+ 'table really is empty.');
180
+ return;
181
+ }
182
+ const orphans = (0, recordingCleanup_1.selectOrphanDirectories)({
183
+ deviceDirs,
184
+ groupDirs,
185
+ livePaths: survivors.map((r) => r.file_path).filter((p) => !!p),
186
+ liveGroupIds: survivors.map((r) => r.group_id).filter((g) => !!g),
187
+ });
188
+ let removed = 0;
189
+ for (const dir of orphans) {
190
+ if (this.removeRecordingDirectory(dir))
191
+ removed++;
192
+ }
193
+ if (removed > 0) {
194
+ this.log.info(`Swept ${removed} unreachable recording director${removed === 1 ? 'y' : 'ies'}.`);
195
+ }
196
+ }
197
+ /** rm -rf a recording directory, refusing anything outside the tree. */
198
+ removeRecordingDirectory(dir) {
199
+ const base = path_1.default.resolve(config_1.config.recordingsAssetsPath);
200
+ const target = path_1.default.resolve(dir);
201
+ if (!target.startsWith(base + path_1.default.sep)) {
202
+ this.log.warn(`Refusing to remove recording dir outside the tree: ${dir}`);
203
+ return false;
204
+ }
205
+ try {
206
+ fs_1.default.rmSync(target, { recursive: true, force: true });
207
+ this.log.debug(`Removed orphan recording directory: ${target}`);
208
+ return true;
209
+ }
210
+ catch (err) {
211
+ this.log.warn(`Failed to remove recording dir ${target}: ${err.message}`);
212
+ return false;
213
+ }
214
+ }
94
215
  /**
95
216
  * Completely purges a build, its sessions, and all associated metadata/assets.
96
217
  */
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.selectExpiredRecordings = selectExpiredRecordings;
7
+ exports.selectOrphanDirectories = selectOrphanDirectories;
8
+ exports.isRecordingSweepSafe = isRecordingSweepSafe;
9
+ const path_1 = __importDefault(require("path"));
10
+ const DAY_MS = 24 * 60 * 60 * 1000;
11
+ /** A recording still being written. Never a deletion candidate. */
12
+ function isInFlight(status) {
13
+ return status === 'RECORDING';
14
+ }
15
+ function ageMs(startedAt, now) {
16
+ if (startedAt === null || startedAt === undefined)
17
+ return undefined;
18
+ const t = startedAt instanceof Date ? startedAt.getTime() : new Date(startedAt).getTime();
19
+ if (!Number.isFinite(t))
20
+ return undefined;
21
+ return now - t;
22
+ }
23
+ /**
24
+ * Ids to purge, given the retention policy. Mirrors the build policy's shape —
25
+ * age AND a count cap — with a separate, shorter window for failures.
26
+ *
27
+ * In-flight rows are excluded from both rules: deleting a row and its file out
28
+ * from under a running ffmpeg would corrupt an active capture, and letting them
29
+ * count toward the cap would mean starting a large group recording evicts
30
+ * finished ones.
31
+ */
32
+ function selectExpiredRecordings(policy) {
33
+ const { rows, now, days, maxCount, failedDays } = policy;
34
+ const expired = new Set();
35
+ const survivors = [];
36
+ for (const r of rows) {
37
+ if (isInFlight(r.status))
38
+ continue;
39
+ const age = ageMs(r.started_at, now);
40
+ // No usable timestamp: leave it alone rather than treat it as epoch 0 and
41
+ // delete it instantly. A retention job must never delete on missing data.
42
+ if (age === undefined)
43
+ continue;
44
+ const limitDays = r.status === 'FAILED' ? failedDays : days;
45
+ if (age > limitDays * DAY_MS) {
46
+ expired.add(r.id);
47
+ }
48
+ else {
49
+ survivors.push({ id: r.id, at: now - age });
50
+ }
51
+ }
52
+ // Count cap applies to what survived the age rules, newest kept.
53
+ if (maxCount >= 0 && survivors.length > maxCount) {
54
+ survivors
55
+ .sort((a, b) => b.at - a.at)
56
+ .slice(maxCount)
57
+ .forEach((s) => expired.add(s.id));
58
+ }
59
+ return [...expired];
60
+ }
61
+ /**
62
+ * Directories no surviving row can reach.
63
+ *
64
+ * **Reachability is the file_path, never the directory name.** The obvious
65
+ * test — delete any directory not named after a `Recording.id` — is wrong and
66
+ * destructive: on the reporter's machine only 35 of 39 rows were id-named, and
67
+ * that test flagged 4 directories holding live recordings (directory
68
+ * `4c9babeb…` belongs to row `c5930c7b…`). A guard caught it before anything
69
+ * was removed; this function encodes the corrected rule.
70
+ */
71
+ function selectOrphanDirectories(scan) {
72
+ const orphans = [];
73
+ const live = scan.livePaths.map((p) => path_1.default.resolve(p));
74
+ for (const dir of scan.deviceDirs) {
75
+ const prefix = path_1.default.resolve(dir) + path_1.default.sep;
76
+ // Compare on a path boundary so /…/ab does not appear to contain /…/abc.
77
+ if (!live.some((p) => p.startsWith(prefix)))
78
+ orphans.push(dir);
79
+ }
80
+ const groups = new Set(scan.liveGroupIds);
81
+ for (const dir of scan.groupDirs) {
82
+ // Composite output IS group-keyed (compositeOutputPath(groupId)), so here
83
+ // the directory name is authoritative — unlike the per-recording dirs.
84
+ if (!groups.has(path_1.default.basename(dir)))
85
+ orphans.push(dir);
86
+ }
87
+ return orphans;
88
+ }
89
+ /**
90
+ * Whether an orphan sweep may proceed.
91
+ *
92
+ * Guards the one failure mode that turns this job into data loss: if the row
93
+ * query fails or is mis-scoped and returns nothing, every directory looks
94
+ * unreachable and the sweep deletes the entire tree. No rows plus no
95
+ * directories is a genuinely empty install and fine.
96
+ */
97
+ function isRecordingSweepSafe(counts) {
98
+ return counts.rowCount > 0 || counts.dirCount === 0;
99
+ }
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const path_1 = __importDefault(require("path"));
8
+ const recordingCleanup_1 = require("../../src/services/recording/recordingCleanup");
9
+ // Issue #209. Live Devices recordings were never removed — no retention policy
10
+ // covered them, there is no DELETE route, and roughly half the directories on
11
+ // disk had no DB row at all (265MB of 313MB on the machine in the report).
12
+ //
13
+ // The decision logic lives here, pure, because the destructive half of a
14
+ // cleanup job is exactly what you cannot safely discover in production.
15
+ const DAY = 24 * 60 * 60 * 1000;
16
+ const NOW = 1800000000000;
17
+ const ago = (days) => new Date(NOW - days * DAY);
18
+ function row(over = {}) {
19
+ return Object.assign({ id: 'r1', status: 'STOPPED', started_at: ago(1) }, over);
20
+ }
21
+ describe('selectExpiredRecordings', () => {
22
+ const base = { now: NOW, days: 30, maxCount: 100, failedDays: 2 };
23
+ it('NEVER expires an in-flight recording, however old', () => {
24
+ // The one rule that must not have exceptions: deleting a row (and file)
25
+ // out from under a running ffmpeg would corrupt an active capture.
26
+ const rows = [row({ id: 'live', status: 'RECORDING', started_at: ago(400) })];
27
+ (0, chai_1.expect)((0, recordingCleanup_1.selectExpiredRecordings)(Object.assign(Object.assign({}, base), { rows }))).to.deep.equal([]);
28
+ });
29
+ it('expires a finished recording past the age window', () => {
30
+ const rows = [row({ id: 'old', started_at: ago(31) }), row({ id: 'new', started_at: ago(29) })];
31
+ (0, chai_1.expect)((0, recordingCleanup_1.selectExpiredRecordings)(Object.assign(Object.assign({}, base), { rows }))).to.deep.equal(['old']);
32
+ });
33
+ it('expires failed recordings on their own, much shorter window', () => {
34
+ // A FAILED row has no playable file — it is noise, not footage.
35
+ const rows = [
36
+ row({ id: 'failed-old', status: 'FAILED', started_at: ago(3) }),
37
+ row({ id: 'failed-new', status: 'FAILED', started_at: ago(1) }),
38
+ row({ id: 'stopped-same-age', status: 'STOPPED', started_at: ago(3) }),
39
+ ];
40
+ (0, chai_1.expect)((0, recordingCleanup_1.selectExpiredRecordings)(Object.assign(Object.assign({}, base), { rows }))).to.deep.equal(['failed-old']);
41
+ });
42
+ it('enforces the count cap oldest-first, like builds', () => {
43
+ const rows = [
44
+ row({ id: 'a', started_at: ago(4) }),
45
+ row({ id: 'b', started_at: ago(3) }),
46
+ row({ id: 'c', started_at: ago(2) }),
47
+ row({ id: 'd', started_at: ago(1) }),
48
+ ];
49
+ (0, chai_1.expect)((0, recordingCleanup_1.selectExpiredRecordings)(Object.assign(Object.assign({}, base), { rows, maxCount: 2 })).sort()).to.deep.equal([
50
+ 'a',
51
+ 'b',
52
+ ]);
53
+ });
54
+ it('does not let in-flight recordings consume the count cap', () => {
55
+ // Otherwise starting a big group recording would evict finished ones.
56
+ const rows = [
57
+ row({ id: 'live1', status: 'RECORDING', started_at: ago(0) }),
58
+ row({ id: 'live2', status: 'RECORDING', started_at: ago(0) }),
59
+ row({ id: 'keep', started_at: ago(1) }),
60
+ ];
61
+ (0, chai_1.expect)((0, recordingCleanup_1.selectExpiredRecordings)(Object.assign(Object.assign({}, base), { rows, maxCount: 2 }))).to.deep.equal([]);
62
+ });
63
+ it('combines both rules without double-counting', () => {
64
+ const rows = [
65
+ row({ id: 'ancient', started_at: ago(90) }),
66
+ row({ id: 'x', started_at: ago(3) }),
67
+ row({ id: 'y', started_at: ago(2) }),
68
+ row({ id: 'z', started_at: ago(1) }),
69
+ ];
70
+ const out = (0, recordingCleanup_1.selectExpiredRecordings)(Object.assign(Object.assign({}, base), { rows, maxCount: 2 }));
71
+ (0, chai_1.expect)(out.sort()).to.deep.equal(['ancient', 'x']);
72
+ (0, chai_1.expect)(new Set(out).size, 'no id may appear twice').to.equal(out.length);
73
+ });
74
+ it('treats a missing started_at as un-expirable rather than ancient', () => {
75
+ // A null date must not read as epoch 0 and delete the row instantly.
76
+ const rows = [row({ id: 'nodate', started_at: null })];
77
+ (0, chai_1.expect)((0, recordingCleanup_1.selectExpiredRecordings)(Object.assign(Object.assign({}, base), { rows }))).to.deep.equal([]);
78
+ });
79
+ });
80
+ describe('selectOrphanDirectories', () => {
81
+ const R = '/assets/recordings';
82
+ const dir = (n) => path_1.default.join(R, n);
83
+ const grpDir = (n) => path_1.default.join(R, '_groups', n);
84
+ it('keeps a directory a live row points into', () => {
85
+ const out = (0, recordingCleanup_1.selectOrphanDirectories)({
86
+ deviceDirs: [dir('aaa')],
87
+ groupDirs: [],
88
+ livePaths: [path_1.default.join(dir('aaa'), 'video', 'aaa.mp4')],
89
+ liveGroupIds: [],
90
+ });
91
+ (0, chai_1.expect)(out).to.deep.equal([]);
92
+ });
93
+ it('does NOT assume the directory is named after the recording id', () => {
94
+ // The trap that would have destroyed 4 live recordings on the reporter's
95
+ // machine: only 35 of 39 rows were id-named. Directory `4c9babeb…` belonged
96
+ // to row `c5930c7b…`. Reachability is the file_path, never the name.
97
+ const out = (0, recordingCleanup_1.selectOrphanDirectories)({
98
+ deviceDirs: [dir('4c9babeb')],
99
+ groupDirs: [],
100
+ livePaths: [path_1.default.join(dir('4c9babeb'), 'video', 'anything.mp4')],
101
+ liveGroupIds: [],
102
+ });
103
+ (0, chai_1.expect)(out, 'a live file inside it makes it reachable regardless of name').to.deep.equal([]);
104
+ });
105
+ it('flags a directory nothing points into', () => {
106
+ const out = (0, recordingCleanup_1.selectOrphanDirectories)({
107
+ deviceDirs: [dir('ghost')],
108
+ groupDirs: [],
109
+ livePaths: [path_1.default.join(dir('real'), 'video', 'real.mp4')],
110
+ liveGroupIds: [],
111
+ });
112
+ (0, chai_1.expect)(out).to.deep.equal([dir('ghost')]);
113
+ });
114
+ it('does not confuse a sibling whose name shares a prefix', () => {
115
+ // /assets/recordings/ab must not count as containing /assets/recordings/abc/...
116
+ const out = (0, recordingCleanup_1.selectOrphanDirectories)({
117
+ deviceDirs: [dir('ab')],
118
+ groupDirs: [],
119
+ livePaths: [path_1.default.join(dir('abc'), 'video', 'v.mp4')],
120
+ liveGroupIds: [],
121
+ });
122
+ (0, chai_1.expect)(out, 'prefix match must be on a path boundary').to.deep.equal([dir('ab')]);
123
+ });
124
+ it('keeps a group directory whose id still has recordings', () => {
125
+ const out = (0, recordingCleanup_1.selectOrphanDirectories)({
126
+ deviceDirs: [],
127
+ groupDirs: [grpDir('g1'), grpDir('g2')],
128
+ livePaths: [],
129
+ liveGroupIds: ['g1'],
130
+ });
131
+ (0, chai_1.expect)(out).to.deep.equal([grpDir('g2')]);
132
+ });
133
+ });
134
+ describe('isRecordingSweepSafe', () => {
135
+ it('refuses to sweep when the row query came back empty but files exist', () => {
136
+ // The failure mode that turns a cleanup job into data loss: a failed or
137
+ // mis-scoped DB read makes every directory look unreachable.
138
+ (0, chai_1.expect)((0, recordingCleanup_1.isRecordingSweepSafe)({ rowCount: 0, dirCount: 42 })).to.equal(false);
139
+ });
140
+ it('allows an empty sweep on a genuinely empty install', () => {
141
+ (0, chai_1.expect)((0, recordingCleanup_1.isRecordingSweepSafe)({ rowCount: 0, dirCount: 0 })).to.equal(true);
142
+ });
143
+ it('allows a normal sweep', () => {
144
+ (0, chai_1.expect)((0, recordingCleanup_1.isRecordingSweepSafe)({ rowCount: 39, dirCount: 78 })).to.equal(true);
145
+ });
146
+ });
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ // Adding a plugin arg to schema.json's `required` list is a BREAKING change for
10
+ // every existing config file. Appium validates the config against this schema
11
+ // and refuses to start when a required key is absent:
12
+ //
13
+ // Fatal Error: Errors in config file .../<id>.yaml:
14
+ // REQUIRED must have required property 'recordingFailedCleanupDays'
15
+ // Process exited (code=2)
16
+ //
17
+ // That is exactly what shipping the recording-retention args as `required` did
18
+ // in 1.12.0 — an already-running server would not come back up after upgrading,
19
+ // with no way to tell from the message that the fix is to edit a YAML file.
20
+ //
21
+ // An arg with a default must be optional: the schema default and the code's own
22
+ // destructuring fallback already supply the value.
23
+ const schema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'schema.json'), 'utf8'));
24
+ /** Args already both required and defaulted when this guard was added. */
25
+ const KNOWN_REQUIRED_WITH_DEFAULT = 23;
26
+ describe('schema.json required args', () => {
27
+ it('does not grow the set of args that are required despite having a default', () => {
28
+ // A default means "you may omit this", which is the opposite of required.
29
+ // 23 args already sit in both lists; they only work because the Mac app
30
+ // writes every required key into the config it generates, so a hand-written
31
+ // or older config is already fragile against them. That is pre-existing
32
+ // debt, not something to fix in a hotfix — but the set must not GROW,
33
+ // because each addition breaks every config written before it.
34
+ const contradictory = schema.required.filter((key) => schema.properties[key] && 'default' in schema.properties[key]);
35
+ (0, chai_1.expect)(contradictory.length, `New required-with-default arg(s): ${contradictory.slice(KNOWN_REQUIRED_WITH_DEFAULT).join(', ')}. ` +
36
+ 'An existing config that omits them fails Appium config validation and the server ' +
37
+ 'will not start. Give the arg a default and leave it out of `required`.').to.equal(KNOWN_REQUIRED_WITH_DEFAULT);
38
+ });
39
+ it('keeps every required arg present in properties', () => {
40
+ const missing = schema.required.filter((key) => !schema.properties[key]);
41
+ (0, chai_1.expect)(missing, 'required lists an arg that does not exist').to.deep.equal([]);
42
+ });
43
+ it('does not require the recording retention args', () => {
44
+ // Named explicitly: this is the regression that took a live server down.
45
+ for (const key of [
46
+ 'recordingCleanupDays',
47
+ 'recordingCleanupMaxCount',
48
+ 'recordingFailedCleanupDays',
49
+ ]) {
50
+ (0, chai_1.expect)(schema.properties[key], `${key} should exist`).to.be.an('object');
51
+ (0, chai_1.expect)(schema.required, `${key} must stay optional`).to.not.include(key);
52
+ }
53
+ });
54
+ });
@@ -1 +1 @@
1
- {"root":["../src/InternalHttpClient.ts","../src/PluginContext.ts","../src/XenonCapabilityManager.ts","../src/chromeUtils.ts","../src/config.ts","../src/device-utils.ts","../src/index.ts","../src/logger.ts","../src/plugin.ts","../src/prisma.ts","../src/app/index.ts","../src/app/swagger-docs.ts","../src/app/swagger.ts","../src/app/routers/androidH264Config.ts","../src/app/routers/apikeys.ts","../src/app/routers/apps.ts","../src/app/routers/audit.ts","../src/app/routers/auth.ts","../src/app/routers/bug-report.ts","../src/app/routers/build-export.ts","../src/app/routers/capabilities.ts","../src/app/routers/config.ts","../src/app/routers/control.ts","../src/app/routers/dashboard.ts","../src/app/routers/grid.ts","../src/app/routers/interceptor.ts","../src/app/routers/iosStreamPort.ts","../src/app/routers/ports.ts","../src/app/routers/processes.ts","../src/app/routers/profile.ts","../src/app/routers/profileIdentity.ts","../src/app/routers/projects.ts","../src/app/routers/recordings.ts","../src/app/routers/reservation.ts","../src/app/routers/sdk-leases.ts","../src/app/routers/sdk-version.ts","../src/app/routers/streamType.ts","../src/app/routers/teams.ts","../src/app/routers/users.ts","../src/app/routers/webhook.ts","../src/app/ws/h264StreamWs.ts","../src/commands/handle.ts","../src/commands/index.ts","../src/dashboard/asset-manager.ts","../src/dashboard/commands.ts","../src/dashboard/event-manager.ts","../src/dashboard/services/app-service.ts","../src/dashboard/services/failure-analysis-service.ts","../src/dashboard/services/session-service.ts","../src/data-service/CircuitBreaker.ts","../src/data-service/config-service.ts","../src/data-service/db.ts","../src/data-service/device-service.ts","../src/data-service/device-store.interface.ts","../src/data-service/device-store.ts","../src/data-service/pending-sessions-service.ts","../src/data-service/pluginArgs.ts","../src/data-service/prisma-service.ts","../src/data-service/prisma-store.ts","../src/data-service/queue-service.ts","../src/data-service/web-config-service.ts","../src/device-managers/AndroidDeviceManager.ts","../src/device-managers/ChromeDriverManager.ts","../src/device-managers/HealthMonitorService.ts","../src/device-managers/IOSDeviceManager.ts","../src/device-managers/NodeDevices.ts","../src/device-managers/iOSTracker.ts","../src/device-managers/index.ts","../src/device-managers/android/AndroidH264StreamService.ts","../src/device-managers/android/AndroidStreamService.ts","../src/device-managers/android/DeviceLockManager.ts","../src/device-managers/android/H264Multiplexer.ts","../src/device-managers/android/ScrcpyServerSession.ts","../src/device-managers/android/androidStreamReuse.ts","../src/device-managers/android/h264NalParser.ts","../src/device-managers/android/scrcpyVersion.ts","../src/device-managers/cloud/CapabilityManager.ts","../src/device-managers/cloud/Devices.ts","../src/device-managers/ios/IOSDiscoveryService.ts","../src/device-managers/ios/IOSStreamService.ts","../src/device-managers/ios/WDAClient.ts","../src/device-managers/ios/iosBattery.ts","../src/device-managers/ios/iosStreamDiagnostics.ts","../src/device-managers/ios/tunnelProcess.ts","../src/enums/Capabilities.ts","../src/enums/Cloud.ts","../src/enums/Platform.ts","../src/enums/SessionType.ts","../src/enums/SocketEvents.ts","../src/generated/client/default.d.ts","../src/generated/client/default.js","../src/generated/client/edge.d.ts","../src/generated/client/edge.js","../src/generated/client/index-browser.js","../src/generated/client/index.d.ts","../src/generated/client/index.js","../src/generated/client/wasm.d.ts","../src/generated/client/wasm.js","../src/generated/client/runtime/edge-esm.js","../src/generated/client/runtime/edge.js","../src/generated/client/runtime/index-browser.d.ts","../src/generated/client/runtime/index-browser.js","../src/generated/client/runtime/library.d.ts","../src/generated/client/runtime/library.js","../src/generated/client/runtime/react-native.js","../src/generated/client/runtime/wasm.js","../src/helpers/UniversalMjpegProxy.ts","../src/helpers/ffmpegPath.ts","../src/helpers/index.ts","../src/helpers/networkAddresses.ts","../src/helpers/singleFlight.ts","../src/interceptors/CommandInterceptor.ts","../src/interfaces/ICloudManager.ts","../src/interfaces/IDevice.ts","../src/interfaces/IDeviceFilterOptions.ts","../src/interfaces/IDeviceManager.ts","../src/interfaces/IOptions.ts","../src/interfaces/IPluginArgs.ts","../src/interfaces/ISessionCapability.ts","../src/logging/sessionContext.ts","../src/middleware/authMiddleware.ts","../src/middleware/csrfMiddleware.ts","../src/middleware/leaseTokenMiddleware.ts","../src/middleware/loginRateLimiter.ts","../src/middleware/rateLimitMiddleware.ts","../src/middleware/roleGuard.ts","../src/middleware/scopeGuard.ts","../src/profiling/AndroidAppProfiler.ts","../src/proxy/wd-command-proxy.ts","../src/scripts/generate-database-migration.ts","../src/scripts/goIosVersion.ts","../src/scripts/initialize-database.ts","../src/scripts/install-go-ios.ts","../src/scripts/prepare-prisma.ts","../src/scripts/run-migrations.ts","../src/services/AICommandService.ts","../src/services/AIService.ts","../src/services/ApiKeyService.ts","../src/services/CircuitBreaker.ts","../src/services/CleanupService.ts","../src/services/DeviceReconciler.ts","../src/services/EmailService.ts","../src/services/EventBus.ts","../src/services/EventLogService.ts","../src/services/InspectorService.ts","../src/services/InterceptorService.ts","../src/services/MetricsService.ts","../src/services/NetworkConditioningService.ts","../src/services/NotificationService.ts","../src/services/OrphanSweeper.ts","../src/services/PasswordResetService.ts","../src/services/PortAllocator.ts","../src/services/ProcessMetricsService.ts","../src/services/ProcessRegistry.ts","../src/services/RequestLogService.ts","../src/services/ResourceIsolationService.ts","../src/services/SecurityService.ts","../src/services/SelectorStateService.ts","../src/services/SelectorVerificationJob.ts","../src/services/ServerManager.ts","../src/services/SessionHeartbeatService.ts","../src/services/SessionLifecycleService.ts","../src/services/ShutdownCoordinator.ts","../src/services/SocketClient.ts","../src/services/SocketServer.ts","../src/services/TeamService.ts","../src/services/TracingService.ts","../src/services/UserService.ts","../src/services/UserSessionService.ts","../src/services/VideoPipelineService.ts","../src/services/sessionTokenGate.ts","../src/services/artifacts/ArtifactStore.ts","../src/services/autowait/AutowaitService.ts","../src/services/autowait/waitFor.ts","../src/services/bug-report/BugReportService.ts","../src/services/bug-report/archive.ts","../src/services/bug-report/har-collector.ts","../src/services/bug-report/manifest.ts","../src/services/bug-report/readme.ts","../src/services/bug-report/types.ts","../src/services/bug-report/video-slice.ts","../src/services/bug-report/window.ts","../src/services/healing/FuzzyXmlHealingProvider.ts","../src/services/healing/HealEtalonService.ts","../src/services/healing/HealedLocatorGenerator.ts","../src/services/healing/HealingMetrics.ts","../src/services/healing/HealingOrchestrator.ts","../src/services/healing/LlmHealingProvider.ts","../src/services/healing/OcrHealingProvider.ts","../src/services/healing/ResilioTreeHealingProvider.ts","../src/services/healing/VisualAiHealingProvider.ts","../src/services/healing/types.ts","../src/services/identity/bootstrap.ts","../src/services/identity/sessionCleanupCron.ts","../src/services/identity/userAuthorization.ts","../src/services/interceptor/AndroidProxyAdapter.ts","../src/services/interceptor/CertManager.ts","../src/services/interceptor/HarBuilder.ts","../src/services/interceptor/HostFilter.ts","../src/services/interceptor/MitmProxyHost.ts","../src/services/interceptor/MockEngine.ts","../src/services/interceptor/RequestBuffer.ts","../src/services/interceptor/SessionArchive.ts","../src/services/interceptor/types.ts","../src/services/lease/LeaseOrphanSweeper.ts","../src/services/lease/LeaseService.ts","../src/services/lease/buildCapabilityBag.ts","../src/services/lease/leaseToken.ts","../src/services/omni-vision/OmniVisionService.ts","../src/services/omni-vision/VisionAssertionService.ts","../src/services/ports/PortAllocatorClient.ts","../src/services/ports/PortAllocatorService.ts","../src/services/recording/RecordingOrchestrator.ts","../src/services/recording/annotation-render.ts","../src/services/recording/busy-precheck.ts","../src/services/recording/concurrency-gate.ts","../src/services/recording/ensureMjpegForRecording.ts","../src/services/recording/manualLock.ts","../src/services/recording/probeDuration.ts","../src/services/recording/proof-bundle.ts","../src/services/recording/recording-store.ts","../src/services/telemetry/attributes.ts","../src/services/token/JwtKeyService.ts","../src/services/token/StreamTicketService.ts","../src/services/token/mcpScopes.ts","../src/sessions/CloudSession.ts","../src/sessions/LocalSession.ts","../src/sessions/RemoteSession.ts","../src/sessions/SessionManager.ts","../src/sessions/XenonSession.ts","../src/types/CLIArgs.ts","../src/types/CloudArgs.ts","../src/types/CloudSchema.ts","../src/types/DeviceType.ts","../src/types/DeviceUpdate.ts","../src/types/IOSDevice.ts","../src/types/Platform.ts","../src/types/SessionStatus.ts","../src/types/express.d.ts","../src/types/healing.d.ts","../src/types/identity.ts","../src/validators/CapabilityValidator.ts","../test/e2e/e2ehelper.ts","../test/e2e/plugin-harness.ts","../test/e2e/plugin.spec.ts","../test/e2e/startup-verification.spec.ts","../test/e2e/telemetry_verification.spec.ts","../test/e2e/video-recording-test.spec.js","../test/e2e/android/conf.spec.js","../test/e2e/android/conf2.spec.js","../test/e2e/android/conf3.spec.js","../test/e2e/hubnode/forward-request.spec.ts","../test/e2e/hubnode/hubnode.spec.ts","../test/e2e/ios/conf1.spec.js","../test/e2e/ios/conf2.spec.js","../test/helpers/seedUser.ts","../test/helpers/test-container.ts","../test/integration/androidDevices.spec.ts","../test/integration/audit-ingest.spec.ts","../test/integration/auth-flow.spec.ts","../test/integration/auth-rate-limit.spec.ts","../test/integration/bug-report-route.spec.ts","../test/integration/cliArgs.js","../test/integration/cross-workflow.spec.ts","../test/integration/forgot-password-rate-limit.spec.ts","../test/integration/forgot-password.spec.ts","../test/integration/healing-events-sessionfilter.spec.ts","../test/integration/legacy-deprecation-headers.spec.ts","../test/integration/migration.spec.ts","../test/integration/node-pair-auth-rest.spec.ts","../test/integration/ports-router.spec.ts","../test/integration/profile-tokens.spec.ts","../test/integration/recordings-router.spec.ts","../test/integration/reset-revokes-sessions.spec.ts","../test/integration/role-matrix.spec.ts","../test/integration/sdk-leases-router.spec.ts","../test/integration/sdk-version.spec.ts","../test/integration/selector-health-endpoint.spec.ts","../test/integration/selector-state-flow.spec.ts","../test/integration/team-membership-migration.spec.ts","../test/integration/team-visibility-dashboard.spec.ts","../test/integration/team-visibility-grid.spec.ts","../test/integration/team-visibility-recordings.spec.ts","../test/integration/users-flow.spec.ts","../test/integration/users-lockout.spec.ts","../test/integration/ios/01iOSSimulator.spec.ts","../test/integration/ios/02iOSDevices.spec.ts","../test/unit/AndroidDeviceManager.spec.ts","../test/unit/ApiKeyService.test.ts","../test/unit/ArtifactStore.spec.ts","../test/unit/AutowaitService.spec.ts","../test/unit/CapabilitiesRouter.spec.ts","../test/unit/ChromeDriverManager.spec.js","../test/unit/CleanupService.spec.ts","../test/unit/CommandInterceptor.autowait.spec.ts","../test/unit/DeviceModel.spec.ts","../test/unit/EmailService.test.ts","../test/unit/EventLogService.spec.ts","../test/unit/FuzzyXmlHealingProvider.test.ts","../test/unit/GetAdbOriginal.js","../test/unit/HealingCascade.test.ts","../test/unit/IOSDeviceManager.spec.js","../test/unit/JwtKeyService.spec.ts","../test/unit/McpScopes.spec.ts","../test/unit/OrphanSweeper.test.ts","../test/unit/PasswordResetService.test.ts","../test/unit/PortAllocator.test.ts","../test/unit/ProcessRegistry.test.ts","../test/unit/ProjectsRouter.spec.ts","../test/unit/RemoteIOs.spec.js","../test/unit/ResilioTreeHealingProvider.test.ts","../test/unit/SessionHeartbeatWrite.test.ts","../test/unit/SessionLifecycleCaps.spec.ts","../test/unit/SessionTokenGate.spec.ts","../test/unit/StreamTicketService.spec.ts","../test/unit/TeamService.test.ts","../test/unit/TokenEndpoint.spec.ts","../test/unit/UserService.test.ts","../test/unit/UserSessionService.test.ts","../test/unit/XenonCapabilityManager.spec.ts","../test/unit/aggregate-hotspots-tuple-key.spec.ts","../test/unit/android-h264-config.spec.ts","../test/unit/android-h264-service.spec.ts","../test/unit/android-stream-adb-path.spec.ts","../test/unit/android-stream-encoder.spec.ts","../test/unit/android-stream-fast-start.spec.ts","../test/unit/android-stream-release.spec.ts","../test/unit/android-stream-reuse-service.spec.ts","../test/unit/android-stream-reuse.spec.ts","../test/unit/android-stream-stall-eof.spec.ts","../test/unit/annotation-render.spec.ts","../test/unit/auth-me-auth-disabled.test.ts","../test/unit/authMiddleware.bearer.spec.ts","../test/unit/authMiddleware.mcpAudience.spec.ts","../test/unit/authMiddleware.streamTicket.spec.ts","../test/unit/authMiddleware.test.ts","../test/unit/bootstrap-identity.test.ts","../test/unit/build-export.spec.ts","../test/unit/busy-precheck.spec.ts","../test/unit/commands.spec.js","../test/unit/concurrency-gate.spec.ts","../test/unit/config.spec.js","../test/unit/device-service.spec.ts","../test/unit/device-utils.spec.ts","../test/unit/enrich-devices-team-names.test.ts","../test/unit/event-manager-regression-hook.spec.ts","../test/unit/extract-access-key-token-pair.test.ts","../test/unit/ffmpeg-path.spec.ts","../test/unit/findAndLockDevice-leasemerge.spec.ts","../test/unit/go-ios-version.spec.ts","../test/unit/h264-multiplexer.spec.ts","../test/unit/h264-nal-parser.spec.ts","../test/unit/h264-ws-frame.spec.ts","../test/unit/healing-instrumentation.spec.ts","../test/unit/healing-state-endpoints.spec.ts","../test/unit/healing-tier-gate.spec.ts","../test/unit/helpers.spec.ts","../test/unit/ios-stream-port.spec.ts","../test/unit/ios-tunnel-process.spec.ts","../test/unit/iosBattery.spec.ts","../test/unit/iosStreamDiagnostics.spec.ts","../test/unit/loginRateLimiter.test.ts","../test/unit/manualLock.test.ts","../test/unit/mjpeg-proxy-cache.spec.ts","../test/unit/network-addresses.test.ts","../test/unit/normalize-eol.spec.js","../test/unit/omni-vision.spec.ts","../test/unit/plugin.spec.ts","../test/unit/prisma.spec.ts","../test/unit/probe-duration.spec.ts","../test/unit/profile-router.test.ts","../test/unit/profileIdentity.spec.ts","../test/unit/proof-bundle.spec.ts","../test/unit/rateLimitMiddleware.test.ts","../test/unit/recording-instrumentation.spec.ts","../test/unit/recording-orchestrator.spec.ts","../test/unit/recording-source-ended.spec.ts","../test/unit/recording-store.spec.ts","../test/unit/remux-to-standard-mp4.spec.ts","../test/unit/roleGuard.test.ts","../test/unit/run-migrations-fail-fast.test.ts","../test/unit/run-migrations.test.ts","../test/unit/scrcpy-server-session.spec.ts","../test/unit/scrcpy-version.spec.ts","../test/unit/selector-state-service.spec.ts","../test/unit/selector-verification-job.spec.ts","../test/unit/single-flight.spec.ts","../test/unit/socketServer.bearer.spec.ts","../test/unit/stream-status-type.spec.ts","../test/unit/team-filter.test.ts","../test/unit/tracing-service-init.spec.ts","../test/unit/userAuthorization.test.ts","../test/unit/users-router.test.ts","../test/unit/video-pipeline-args.spec.ts","../test/unit/video-pipeline-exit.spec.ts","../test/unit/video-pipeline-outputpath.spec.ts","../test/unit/bug-report/archive.spec.ts","../test/unit/bug-report/har-collector.spec.ts","../test/unit/bug-report/manifest.spec.ts","../test/unit/bug-report/readme.spec.ts","../test/unit/bug-report/route.spec.ts","../test/unit/bug-report/service.spec.ts","../test/unit/bug-report/video-slice.spec.ts","../test/unit/bug-report/window.spec.ts","../test/unit/fixtures/device.config.js","../test/unit/fixtures/devices.js","../test/unit/interceptor/AndroidProxyAdapter.spec.ts","../test/unit/interceptor/CertManager.spec.ts","../test/unit/interceptor/HarBuilder.spec.ts","../test/unit/interceptor/HostFilter.spec.ts","../test/unit/interceptor/MitmProxyHost.spec.ts","../test/unit/interceptor/MockEngine.spec.ts","../test/unit/interceptor/RequestBuffer.spec.ts","../test/unit/interceptor/SessionArchive.spec.ts","../test/unit/lease/LeaseOrphanSweeper.spec.ts","../test/unit/lease/LeaseService.spec.ts","../test/unit/lease/buildCapabilityBag.spec.ts","../test/unit/lease/leaseToken.spec.ts","../test/unit/lease/nodePairAuth.spec.ts","../test/unit/ports/PortAllocatorService.spec.ts"],"version":"5.9.3"}
1
+ {"root":["../src/InternalHttpClient.ts","../src/PluginContext.ts","../src/XenonCapabilityManager.ts","../src/chromeUtils.ts","../src/config.ts","../src/device-utils.ts","../src/index.ts","../src/logger.ts","../src/plugin.ts","../src/prisma.ts","../src/app/index.ts","../src/app/swagger-docs.ts","../src/app/swagger.ts","../src/app/routers/androidH264Config.ts","../src/app/routers/apikeys.ts","../src/app/routers/apps.ts","../src/app/routers/audit.ts","../src/app/routers/auth.ts","../src/app/routers/bug-report.ts","../src/app/routers/build-export.ts","../src/app/routers/capabilities.ts","../src/app/routers/config.ts","../src/app/routers/control.ts","../src/app/routers/dashboard.ts","../src/app/routers/grid.ts","../src/app/routers/interceptor.ts","../src/app/routers/iosStreamPort.ts","../src/app/routers/ports.ts","../src/app/routers/processes.ts","../src/app/routers/profile.ts","../src/app/routers/profileIdentity.ts","../src/app/routers/projects.ts","../src/app/routers/recordings.ts","../src/app/routers/reservation.ts","../src/app/routers/sdk-leases.ts","../src/app/routers/sdk-version.ts","../src/app/routers/streamType.ts","../src/app/routers/teams.ts","../src/app/routers/users.ts","../src/app/routers/webhook.ts","../src/app/ws/h264StreamWs.ts","../src/commands/handle.ts","../src/commands/index.ts","../src/dashboard/asset-manager.ts","../src/dashboard/commands.ts","../src/dashboard/event-manager.ts","../src/dashboard/services/app-service.ts","../src/dashboard/services/failure-analysis-service.ts","../src/dashboard/services/session-service.ts","../src/data-service/CircuitBreaker.ts","../src/data-service/config-service.ts","../src/data-service/db.ts","../src/data-service/device-service.ts","../src/data-service/device-store.interface.ts","../src/data-service/device-store.ts","../src/data-service/pending-sessions-service.ts","../src/data-service/pluginArgs.ts","../src/data-service/prisma-service.ts","../src/data-service/prisma-store.ts","../src/data-service/queue-service.ts","../src/data-service/web-config-service.ts","../src/device-managers/AndroidDeviceManager.ts","../src/device-managers/ChromeDriverManager.ts","../src/device-managers/HealthMonitorService.ts","../src/device-managers/IOSDeviceManager.ts","../src/device-managers/NodeDevices.ts","../src/device-managers/iOSTracker.ts","../src/device-managers/index.ts","../src/device-managers/android/AndroidH264StreamService.ts","../src/device-managers/android/AndroidStreamService.ts","../src/device-managers/android/DeviceLockManager.ts","../src/device-managers/android/H264Multiplexer.ts","../src/device-managers/android/ScrcpyServerSession.ts","../src/device-managers/android/androidStreamReuse.ts","../src/device-managers/android/h264NalParser.ts","../src/device-managers/android/scrcpyVersion.ts","../src/device-managers/cloud/CapabilityManager.ts","../src/device-managers/cloud/Devices.ts","../src/device-managers/ios/IOSDiscoveryService.ts","../src/device-managers/ios/IOSStreamService.ts","../src/device-managers/ios/WDAClient.ts","../src/device-managers/ios/iosBattery.ts","../src/device-managers/ios/iosStreamDiagnostics.ts","../src/device-managers/ios/tunnelProcess.ts","../src/enums/Capabilities.ts","../src/enums/Cloud.ts","../src/enums/Platform.ts","../src/enums/SessionType.ts","../src/enums/SocketEvents.ts","../src/generated/client/default.d.ts","../src/generated/client/default.js","../src/generated/client/edge.d.ts","../src/generated/client/edge.js","../src/generated/client/index-browser.js","../src/generated/client/index.d.ts","../src/generated/client/index.js","../src/generated/client/wasm.d.ts","../src/generated/client/wasm.js","../src/generated/client/runtime/edge-esm.js","../src/generated/client/runtime/edge.js","../src/generated/client/runtime/index-browser.d.ts","../src/generated/client/runtime/index-browser.js","../src/generated/client/runtime/library.d.ts","../src/generated/client/runtime/library.js","../src/generated/client/runtime/react-native.js","../src/generated/client/runtime/wasm.js","../src/helpers/UniversalMjpegProxy.ts","../src/helpers/ffmpegPath.ts","../src/helpers/index.ts","../src/helpers/networkAddresses.ts","../src/helpers/singleFlight.ts","../src/interceptors/CommandInterceptor.ts","../src/interfaces/ICloudManager.ts","../src/interfaces/IDevice.ts","../src/interfaces/IDeviceFilterOptions.ts","../src/interfaces/IDeviceManager.ts","../src/interfaces/IOptions.ts","../src/interfaces/IPluginArgs.ts","../src/interfaces/ISessionCapability.ts","../src/logging/sessionContext.ts","../src/middleware/authMiddleware.ts","../src/middleware/csrfMiddleware.ts","../src/middleware/leaseTokenMiddleware.ts","../src/middleware/loginRateLimiter.ts","../src/middleware/rateLimitMiddleware.ts","../src/middleware/roleGuard.ts","../src/middleware/scopeGuard.ts","../src/profiling/AndroidAppProfiler.ts","../src/proxy/wd-command-proxy.ts","../src/scripts/generate-database-migration.ts","../src/scripts/goIosVersion.ts","../src/scripts/initialize-database.ts","../src/scripts/install-go-ios.ts","../src/scripts/prepare-prisma.ts","../src/scripts/run-migrations.ts","../src/services/AICommandService.ts","../src/services/AIService.ts","../src/services/ApiKeyService.ts","../src/services/CircuitBreaker.ts","../src/services/CleanupService.ts","../src/services/DeviceReconciler.ts","../src/services/EmailService.ts","../src/services/EventBus.ts","../src/services/EventLogService.ts","../src/services/InspectorService.ts","../src/services/InterceptorService.ts","../src/services/MetricsService.ts","../src/services/NetworkConditioningService.ts","../src/services/NotificationService.ts","../src/services/OrphanSweeper.ts","../src/services/PasswordResetService.ts","../src/services/PortAllocator.ts","../src/services/ProcessMetricsService.ts","../src/services/ProcessRegistry.ts","../src/services/RequestLogService.ts","../src/services/ResourceIsolationService.ts","../src/services/SecurityService.ts","../src/services/SelectorStateService.ts","../src/services/SelectorVerificationJob.ts","../src/services/ServerManager.ts","../src/services/SessionHeartbeatService.ts","../src/services/SessionLifecycleService.ts","../src/services/ShutdownCoordinator.ts","../src/services/SocketClient.ts","../src/services/SocketServer.ts","../src/services/TeamService.ts","../src/services/TracingService.ts","../src/services/UserService.ts","../src/services/UserSessionService.ts","../src/services/VideoPipelineService.ts","../src/services/sessionTokenGate.ts","../src/services/artifacts/ArtifactStore.ts","../src/services/autowait/AutowaitService.ts","../src/services/autowait/waitFor.ts","../src/services/bug-report/BugReportService.ts","../src/services/bug-report/archive.ts","../src/services/bug-report/har-collector.ts","../src/services/bug-report/manifest.ts","../src/services/bug-report/readme.ts","../src/services/bug-report/types.ts","../src/services/bug-report/video-slice.ts","../src/services/bug-report/window.ts","../src/services/healing/FuzzyXmlHealingProvider.ts","../src/services/healing/HealEtalonService.ts","../src/services/healing/HealedLocatorGenerator.ts","../src/services/healing/HealingMetrics.ts","../src/services/healing/HealingOrchestrator.ts","../src/services/healing/LlmHealingProvider.ts","../src/services/healing/OcrHealingProvider.ts","../src/services/healing/ResilioTreeHealingProvider.ts","../src/services/healing/VisualAiHealingProvider.ts","../src/services/healing/types.ts","../src/services/identity/bootstrap.ts","../src/services/identity/sessionCleanupCron.ts","../src/services/identity/userAuthorization.ts","../src/services/interceptor/AndroidProxyAdapter.ts","../src/services/interceptor/CertManager.ts","../src/services/interceptor/HarBuilder.ts","../src/services/interceptor/HostFilter.ts","../src/services/interceptor/MitmProxyHost.ts","../src/services/interceptor/MockEngine.ts","../src/services/interceptor/RequestBuffer.ts","../src/services/interceptor/SessionArchive.ts","../src/services/interceptor/types.ts","../src/services/lease/LeaseOrphanSweeper.ts","../src/services/lease/LeaseService.ts","../src/services/lease/buildCapabilityBag.ts","../src/services/lease/leaseToken.ts","../src/services/omni-vision/OmniVisionService.ts","../src/services/omni-vision/VisionAssertionService.ts","../src/services/ports/PortAllocatorClient.ts","../src/services/ports/PortAllocatorService.ts","../src/services/recording/RecordingOrchestrator.ts","../src/services/recording/annotation-render.ts","../src/services/recording/busy-precheck.ts","../src/services/recording/concurrency-gate.ts","../src/services/recording/ensureMjpegForRecording.ts","../src/services/recording/manualLock.ts","../src/services/recording/probeDuration.ts","../src/services/recording/proof-bundle.ts","../src/services/recording/recording-store.ts","../src/services/recording/recordingCleanup.ts","../src/services/telemetry/attributes.ts","../src/services/token/JwtKeyService.ts","../src/services/token/StreamTicketService.ts","../src/services/token/mcpScopes.ts","../src/sessions/CloudSession.ts","../src/sessions/LocalSession.ts","../src/sessions/RemoteSession.ts","../src/sessions/SessionManager.ts","../src/sessions/XenonSession.ts","../src/types/CLIArgs.ts","../src/types/CloudArgs.ts","../src/types/CloudSchema.ts","../src/types/DeviceType.ts","../src/types/DeviceUpdate.ts","../src/types/IOSDevice.ts","../src/types/Platform.ts","../src/types/SessionStatus.ts","../src/types/express.d.ts","../src/types/healing.d.ts","../src/types/identity.ts","../src/validators/CapabilityValidator.ts","../test/e2e/e2ehelper.ts","../test/e2e/plugin-harness.ts","../test/e2e/plugin.spec.ts","../test/e2e/startup-verification.spec.ts","../test/e2e/telemetry_verification.spec.ts","../test/e2e/video-recording-test.spec.js","../test/e2e/android/conf.spec.js","../test/e2e/android/conf2.spec.js","../test/e2e/android/conf3.spec.js","../test/e2e/hubnode/forward-request.spec.ts","../test/e2e/hubnode/hubnode.spec.ts","../test/e2e/ios/conf1.spec.js","../test/e2e/ios/conf2.spec.js","../test/helpers/seedUser.ts","../test/helpers/test-container.ts","../test/integration/androidDevices.spec.ts","../test/integration/audit-ingest.spec.ts","../test/integration/auth-flow.spec.ts","../test/integration/auth-rate-limit.spec.ts","../test/integration/bug-report-route.spec.ts","../test/integration/cliArgs.js","../test/integration/cross-workflow.spec.ts","../test/integration/forgot-password-rate-limit.spec.ts","../test/integration/forgot-password.spec.ts","../test/integration/healing-events-sessionfilter.spec.ts","../test/integration/legacy-deprecation-headers.spec.ts","../test/integration/migration.spec.ts","../test/integration/node-pair-auth-rest.spec.ts","../test/integration/ports-router.spec.ts","../test/integration/profile-tokens.spec.ts","../test/integration/recordings-router.spec.ts","../test/integration/reset-revokes-sessions.spec.ts","../test/integration/role-matrix.spec.ts","../test/integration/sdk-leases-router.spec.ts","../test/integration/sdk-version.spec.ts","../test/integration/selector-health-endpoint.spec.ts","../test/integration/selector-state-flow.spec.ts","../test/integration/team-membership-migration.spec.ts","../test/integration/team-visibility-dashboard.spec.ts","../test/integration/team-visibility-grid.spec.ts","../test/integration/team-visibility-recordings.spec.ts","../test/integration/users-flow.spec.ts","../test/integration/users-lockout.spec.ts","../test/integration/ios/01iOSSimulator.spec.ts","../test/integration/ios/02iOSDevices.spec.ts","../test/unit/AndroidDeviceManager.spec.ts","../test/unit/ApiKeyService.test.ts","../test/unit/ArtifactStore.spec.ts","../test/unit/AutowaitService.spec.ts","../test/unit/CapabilitiesRouter.spec.ts","../test/unit/ChromeDriverManager.spec.js","../test/unit/CleanupService.spec.ts","../test/unit/CommandInterceptor.autowait.spec.ts","../test/unit/DeviceModel.spec.ts","../test/unit/EmailService.test.ts","../test/unit/EventLogService.spec.ts","../test/unit/FuzzyXmlHealingProvider.test.ts","../test/unit/GetAdbOriginal.js","../test/unit/HealingCascade.test.ts","../test/unit/IOSDeviceManager.spec.js","../test/unit/JwtKeyService.spec.ts","../test/unit/McpScopes.spec.ts","../test/unit/OrphanSweeper.test.ts","../test/unit/PasswordResetService.test.ts","../test/unit/PortAllocator.test.ts","../test/unit/ProcessRegistry.test.ts","../test/unit/ProjectsRouter.spec.ts","../test/unit/RemoteIOs.spec.js","../test/unit/ResilioTreeHealingProvider.test.ts","../test/unit/SessionHeartbeatWrite.test.ts","../test/unit/SessionLifecycleCaps.spec.ts","../test/unit/SessionTokenGate.spec.ts","../test/unit/StreamTicketService.spec.ts","../test/unit/TeamService.test.ts","../test/unit/TokenEndpoint.spec.ts","../test/unit/UserService.test.ts","../test/unit/UserSessionService.test.ts","../test/unit/XenonCapabilityManager.spec.ts","../test/unit/aggregate-hotspots-tuple-key.spec.ts","../test/unit/android-h264-config.spec.ts","../test/unit/android-h264-service.spec.ts","../test/unit/android-stream-adb-path.spec.ts","../test/unit/android-stream-encoder.spec.ts","../test/unit/android-stream-fast-start.spec.ts","../test/unit/android-stream-release.spec.ts","../test/unit/android-stream-reuse-service.spec.ts","../test/unit/android-stream-reuse.spec.ts","../test/unit/android-stream-stall-eof.spec.ts","../test/unit/annotation-render.spec.ts","../test/unit/auth-me-auth-disabled.test.ts","../test/unit/authMiddleware.bearer.spec.ts","../test/unit/authMiddleware.mcpAudience.spec.ts","../test/unit/authMiddleware.streamTicket.spec.ts","../test/unit/authMiddleware.test.ts","../test/unit/bootstrap-identity.test.ts","../test/unit/build-export.spec.ts","../test/unit/busy-precheck.spec.ts","../test/unit/commands.spec.js","../test/unit/concurrency-gate.spec.ts","../test/unit/config.spec.js","../test/unit/device-service.spec.ts","../test/unit/device-utils.spec.ts","../test/unit/enrich-devices-team-names.test.ts","../test/unit/event-manager-regression-hook.spec.ts","../test/unit/extract-access-key-token-pair.test.ts","../test/unit/ffmpeg-path.spec.ts","../test/unit/findAndLockDevice-leasemerge.spec.ts","../test/unit/go-ios-version.spec.ts","../test/unit/h264-multiplexer.spec.ts","../test/unit/h264-nal-parser.spec.ts","../test/unit/h264-ws-frame.spec.ts","../test/unit/healing-instrumentation.spec.ts","../test/unit/healing-state-endpoints.spec.ts","../test/unit/healing-tier-gate.spec.ts","../test/unit/helpers.spec.ts","../test/unit/ios-stream-port.spec.ts","../test/unit/ios-tunnel-process.spec.ts","../test/unit/iosBattery.spec.ts","../test/unit/iosStreamDiagnostics.spec.ts","../test/unit/loginRateLimiter.test.ts","../test/unit/manualLock.test.ts","../test/unit/mjpeg-proxy-cache.spec.ts","../test/unit/network-addresses.test.ts","../test/unit/normalize-eol.spec.js","../test/unit/omni-vision.spec.ts","../test/unit/plugin.spec.ts","../test/unit/prisma.spec.ts","../test/unit/probe-duration.spec.ts","../test/unit/profile-router.test.ts","../test/unit/profileIdentity.spec.ts","../test/unit/proof-bundle.spec.ts","../test/unit/rateLimitMiddleware.test.ts","../test/unit/recording-cleanup.spec.ts","../test/unit/recording-instrumentation.spec.ts","../test/unit/recording-orchestrator.spec.ts","../test/unit/recording-source-ended.spec.ts","../test/unit/recording-store.spec.ts","../test/unit/remux-to-standard-mp4.spec.ts","../test/unit/roleGuard.test.ts","../test/unit/run-migrations-fail-fast.test.ts","../test/unit/run-migrations.test.ts","../test/unit/schema-required-args.spec.ts","../test/unit/scrcpy-server-session.spec.ts","../test/unit/scrcpy-version.spec.ts","../test/unit/selector-state-service.spec.ts","../test/unit/selector-verification-job.spec.ts","../test/unit/single-flight.spec.ts","../test/unit/socketServer.bearer.spec.ts","../test/unit/stream-status-type.spec.ts","../test/unit/team-filter.test.ts","../test/unit/tracing-service-init.spec.ts","../test/unit/userAuthorization.test.ts","../test/unit/users-router.test.ts","../test/unit/video-pipeline-args.spec.ts","../test/unit/video-pipeline-exit.spec.ts","../test/unit/video-pipeline-outputpath.spec.ts","../test/unit/bug-report/archive.spec.ts","../test/unit/bug-report/har-collector.spec.ts","../test/unit/bug-report/manifest.spec.ts","../test/unit/bug-report/readme.spec.ts","../test/unit/bug-report/route.spec.ts","../test/unit/bug-report/service.spec.ts","../test/unit/bug-report/video-slice.spec.ts","../test/unit/bug-report/window.spec.ts","../test/unit/fixtures/device.config.js","../test/unit/fixtures/devices.js","../test/unit/interceptor/AndroidProxyAdapter.spec.ts","../test/unit/interceptor/CertManager.spec.ts","../test/unit/interceptor/HarBuilder.spec.ts","../test/unit/interceptor/HostFilter.spec.ts","../test/unit/interceptor/MitmProxyHost.spec.ts","../test/unit/interceptor/MockEngine.spec.ts","../test/unit/interceptor/RequestBuffer.spec.ts","../test/unit/interceptor/SessionArchive.spec.ts","../test/unit/lease/LeaseOrphanSweeper.spec.ts","../test/unit/lease/LeaseService.spec.ts","../test/unit/lease/buildCapabilityBag.spec.ts","../test/unit/lease/leaseToken.spec.ts","../test/unit/lease/nodePairAuth.spec.ts","../test/unit/ports/PortAllocatorService.spec.ts"],"version":"5.9.3"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenon-device-management/xenon",
3
- "version": "1.11.2",
3
+ "version": "1.12.1",
4
4
  "description": "Xenon - Intelligent Mobile Infrastructure. A self-healing device orchestration platform for Appium.",
5
5
  "main": "./lib/src/index.js",
6
6
  "exports": {
package/schema.json CHANGED
@@ -224,6 +224,21 @@
224
224
  "default": true,
225
225
  "description": "When true, the cleanup job also deletes session video recordings and screenshots from disk (not just DB rows)."
226
226
  },
227
+ "recordingCleanupDays": {
228
+ "type": "number",
229
+ "default": 30,
230
+ "description": "Live Devices recordings older than this many days are purged (DB row + files) by the cleanup job. Runs on `buildCleanupSchedule`."
231
+ },
232
+ "recordingCleanupMaxCount": {
233
+ "type": "number",
234
+ "default": 100,
235
+ "description": "Maximum number of recordings to retain. Oldest-first eviction beyond this cap regardless of `recordingCleanupDays`."
236
+ },
237
+ "recordingFailedCleanupDays": {
238
+ "type": "number",
239
+ "default": 2,
240
+ "description": "Failed recordings hold no playable file, so they are purged on this much shorter window than `recordingCleanupDays`."
241
+ },
227
242
  "sessionHeartbeatIntervalMs": {
228
243
  "type": "number",
229
244
  "default": 30000,
@@ -79,6 +79,9 @@ export const DefaultPluginArgs: IPluginArgs = {
79
79
  buildCleanupMaxCount: 100,
80
80
  buildCleanupSchedule: '0 0 * * *',
81
81
  deleteBuildAssets: true,
82
+ recordingCleanupDays: 30,
83
+ recordingCleanupMaxCount: 100,
84
+ recordingFailedCleanupDays: 2,
82
85
  sessionHeartbeatIntervalMs: 30000,
83
86
  enableJsonLogging: false,
84
87
  autowait: {