agent-scenario-loop 0.1.3 → 0.1.5
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/app/profile-session.ts +263 -17
- package/dist/core/artifact-contract.d.ts +6 -4
- package/dist/core/artifact-contract.js +164 -15
- package/dist/core/artifact-layout.d.ts +2 -0
- package/dist/core/artifact-layout.js +2 -0
- package/dist/core/planner.js +4 -3
- package/dist/core/schema-validator.d.ts +1 -0
- package/dist/core/schema-validator.js +1 -0
- package/dist/runner/android-adb-driver.d.ts +7 -2
- package/dist/runner/android-adb-driver.js +7 -1
- package/dist/runner/android-adb.d.ts +40 -5
- package/dist/runner/android-adb.js +1046 -664
- package/dist/runner/ios-simctl.d.ts +1 -0
- package/dist/runner/ios-simctl.js +1 -0
- package/dist/runner/profile-android.d.ts +11 -1
- package/dist/runner/profile-android.js +266 -25
- package/dist/runner/profile-ios.d.ts +3 -2
- package/dist/runner/profile-ios.js +252 -22
- package/dist/runner/profile-mobile.d.ts +63 -4
- package/dist/runner/profile-mobile.js +1002 -20
- package/dist/runner/validate-project.js +3 -0
- package/dist/scripts/consumer-rehearsal.d.ts +127 -0
- package/dist/scripts/consumer-rehearsal.js +774 -0
- package/dist/scripts/downstream-local-package-gate.d.ts +2 -0
- package/dist/scripts/downstream-local-package-gate.js +264 -0
- package/dist/scripts/package-smoke.d.ts +104 -0
- package/dist/scripts/package-smoke.js +2304 -0
- package/dist/scripts/release-check.d.ts +47 -0
- package/dist/scripts/release-check.js +117 -0
- package/dist/scripts/release-readiness.d.ts +2 -0
- package/dist/scripts/release-readiness.js +539 -0
- package/docs/adapters.md +3 -1
- package/docs/api.md +2 -2
- package/docs/authoring.md +34 -2
- package/docs/consumer-rehearsal.md +33 -1
- package/docs/contracts.md +16 -2
- package/docs/live-proofs.md +12 -4
- package/examples/mobile-app/runner-manifests/evidence-provider.json +3 -3
- package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +25 -0
- package/examples/runners/README.md +3 -3
- package/examples/runners/axe-accessibility-provider.json +2 -2
- package/examples/runners/script-accessibility-provider.json +2 -2
- package/examples/runners/script-memory-provider.json +2 -2
- package/examples/runners/script-network-provider.json +2 -2
- package/examples/runners/script-profiler-provider.json +2 -2
- package/package.json +12 -4
- package/schemas/manifest.schema.json +73 -3
- package/schemas/profiler.schema.json +243 -0
- package/schemas/runner-capabilities.schema.json +8 -2
- package/schemas/scenario.schema.json +18 -2
- package/templates/evidence-provider.json +3 -3
- package/templates/scripts/asl-capture-profiler-provider.mjs +20 -0
|
@@ -462,6 +462,7 @@ async function seedProfileSessionStorage({ bundleId, commands = [], dataContaine
|
|
|
462
462
|
...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
|
|
463
463
|
timestamp: typeof profileCommand.timestamp === 'number' ? profileCommand.timestamp : startedAt + index + 1,
|
|
464
464
|
...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
|
|
465
|
+
...(typeof profileCommand.waitMs === 'number' ? { waitMs: profileCommand.waitMs } : {}),
|
|
465
466
|
...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
|
|
466
467
|
}));
|
|
467
468
|
manifest[profileStorageKeys.session] = JSON.stringify(session);
|
|
@@ -35,6 +35,16 @@ declare function resolveProfileSessionCaptureWaitMs({ args, profileSessionEnable
|
|
|
35
35
|
profileSessionEnabled: boolean;
|
|
36
36
|
scenario: Record<string, any>;
|
|
37
37
|
}): number;
|
|
38
|
+
/**
|
|
39
|
+
* Derives a bounded logcat tail large enough to keep command-session evidence.
|
|
40
|
+
*
|
|
41
|
+
* @param {{commands: AndroidAdbProfileCommand[], profileSessionEnabled: boolean}} options
|
|
42
|
+
* @returns {number}
|
|
43
|
+
*/
|
|
44
|
+
declare function deriveProfileSessionLogcatLines({ commands, profileSessionEnabled, }: {
|
|
45
|
+
commands: AndroidAdbProfileCommand[];
|
|
46
|
+
profileSessionEnabled: boolean;
|
|
47
|
+
}): number;
|
|
38
48
|
/**
|
|
39
49
|
* Expands normalized scenario evidence steps into Android adb driver actions.
|
|
40
50
|
*
|
|
@@ -84,4 +94,4 @@ declare function runProfileAndroid(args: import('./profile-mobile').CliArgs, opt
|
|
|
84
94
|
* @returns {Promise<void>}
|
|
85
95
|
*/
|
|
86
96
|
declare function main(): Promise<void>;
|
|
87
|
-
export { deriveProfileSessionCaptureWaitMs, main, parseArgs, resolveAndroidAdbProfileCommands, resolveAndroidAdbDriverSteps, resolveProfileSessionCaptureWaitMs, readAndroidAdbVideoCapturePath, validateAndroidAdbDriverSteps, runProfileAndroid, summarizeFailedAndroidChecks, usage, };
|
|
97
|
+
export { deriveProfileSessionCaptureWaitMs, deriveProfileSessionLogcatLines, main, parseArgs, resolveAndroidAdbProfileCommands, resolveAndroidAdbDriverSteps, resolveProfileSessionCaptureWaitMs, readAndroidAdbVideoCapturePath, validateAndroidAdbDriverSteps, runProfileAndroid, summarizeFailedAndroidChecks, usage, };
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.usage = exports.parseArgs = void 0;
|
|
5
5
|
exports.deriveProfileSessionCaptureWaitMs = deriveProfileSessionCaptureWaitMs;
|
|
6
|
+
exports.deriveProfileSessionLogcatLines = deriveProfileSessionLogcatLines;
|
|
6
7
|
exports.main = main;
|
|
7
8
|
exports.resolveAndroidAdbProfileCommands = resolveAndroidAdbProfileCommands;
|
|
8
9
|
exports.resolveAndroidAdbDriverSteps = resolveAndroidAdbDriverSteps;
|
|
@@ -16,7 +17,7 @@ const fs = require('node:fs');
|
|
|
16
17
|
const path = require('node:path');
|
|
17
18
|
const { hasHelpFlag } = require('./cli');
|
|
18
19
|
const { ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER, parsePositiveInteger, runAndroidAdbPreflight, } = require('./android-adb');
|
|
19
|
-
const { parseArgs, readScalarArg, runProfileMobile, usage, } = require('./profile-mobile');
|
|
20
|
+
const { parseArgs, readScalarArg, resolveArtifactRoot, resolveProfileScenarioName, runProfileCompatibilityPreflight, runProfileMobile, usage, } = require('./profile-mobile');
|
|
20
21
|
exports.parseArgs = parseArgs;
|
|
21
22
|
exports.usage = usage;
|
|
22
23
|
const { buildScenarioExecutionPlan } = require('../core/execution-plan');
|
|
@@ -24,8 +25,12 @@ const { runAgentDeviceCapture } = require('./agent-device');
|
|
|
24
25
|
const { loadAslLocalEnv, readStringArgOrEnv } = require('./local-env');
|
|
25
26
|
const PROFILE_SESSION_CAPTURE_BOOTSTRAP_MS = 1000;
|
|
26
27
|
const PROFILE_SESSION_CAPTURE_MAX_MS = 120000;
|
|
28
|
+
const PROFILE_SESSION_LOGCAT_MIN_LINES = 1000;
|
|
29
|
+
const PROFILE_SESSION_LOGCAT_MAX_LINES = 20000;
|
|
30
|
+
const PROFILE_SESSION_LOGCAT_LINES_PER_COMMAND = 300;
|
|
27
31
|
const DEFAULT_ANDROID_PROFILE_SESSION_STORAGE_KEY = 'agent-scenario-loop.profile-session.1';
|
|
28
32
|
const DEFAULT_ANDROID_PROFILE_COMMAND_STORAGE_KEY = 'agent-scenario-loop.profile-commands.1';
|
|
33
|
+
const DEFAULT_ANDROID_DEV_CLIENT_READY_PATTERN = 'Running "main"';
|
|
29
34
|
const MANIFEST_LIFECYCLE_PHASES = new Set([
|
|
30
35
|
'cold-launch',
|
|
31
36
|
'warm-launch',
|
|
@@ -41,6 +46,17 @@ const MANIFEST_LIFECYCLE_PHASES = new Set([
|
|
|
41
46
|
'reboot',
|
|
42
47
|
'relaunch',
|
|
43
48
|
]);
|
|
49
|
+
const ANDROID_PROFILE_RUNNER_CAPABILITIES = {
|
|
50
|
+
schemaVersion: '1.0.0',
|
|
51
|
+
runnerId: 'android-adb-profile-runner',
|
|
52
|
+
kind: 'primary',
|
|
53
|
+
platforms: ['android'],
|
|
54
|
+
capabilities: ['launch', 'sessionControl', 'command', 'logCapture', 'artifactWrite'],
|
|
55
|
+
driverActions: ['tap', 'scroll', 'assertVisible', 'inspectTree', 'screenshot', 'record', 'readLogs'],
|
|
56
|
+
artifactOutputs: ['logs', 'signals', 'screenshot', 'video', 'uiTree'],
|
|
57
|
+
uiContexts: ['app'],
|
|
58
|
+
lifecycle: ['prepare', 'launch', 'startSession', 'executeStep', 'waitForTruthEvent', 'captureEvidence', 'stopSession', 'finalize'],
|
|
59
|
+
};
|
|
44
60
|
/**
|
|
45
61
|
* Reads and parses a JSON object from disk.
|
|
46
62
|
*
|
|
@@ -150,7 +166,7 @@ function resolveAndroidPackageName({ args, config, }) {
|
|
|
150
166
|
* @param {{config: Record<string, unknown>, action: 'start' | 'command', scenario: string, runId: string, command?: string}} options
|
|
151
167
|
* @returns {string}
|
|
152
168
|
*/
|
|
153
|
-
function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitTimeoutMs, }) {
|
|
169
|
+
function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitMs, waitTimeoutMs, }) {
|
|
154
170
|
const scheme = typeof config.app?.profileSessionScheme === 'string'
|
|
155
171
|
? config.app.profileSessionScheme
|
|
156
172
|
: typeof config.app?.scheme === 'string'
|
|
@@ -171,6 +187,9 @@ function buildProfileSessionUrl({ action, command, commandId, config, queueId, r
|
|
|
171
187
|
if (waitForMilestone) {
|
|
172
188
|
params.set('waitForMilestone', waitForMilestone);
|
|
173
189
|
}
|
|
190
|
+
if (typeof waitMs === 'number') {
|
|
191
|
+
params.set('waitMs', String(waitMs));
|
|
192
|
+
}
|
|
174
193
|
if (typeof waitTimeoutMs === 'number') {
|
|
175
194
|
params.set('waitTimeoutMs', String(waitTimeoutMs));
|
|
176
195
|
}
|
|
@@ -184,11 +203,10 @@ function buildProfileSessionUrl({ action, command, commandId, config, queueId, r
|
|
|
184
203
|
* @returns {import('./android-adb').AndroidAsyncStorageWrite[]}
|
|
185
204
|
*/
|
|
186
205
|
function buildProfileSessionStorageWrites({ commands, commandStorageKey, commandWaitMs, runId, scenario, sessionStorageKey, }) {
|
|
187
|
-
const timestampBase = Date.now();
|
|
188
206
|
const storedCommands = commands.map((profileCommand, index) => {
|
|
189
|
-
const
|
|
207
|
+
const timestampPlaceholder = `${ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER}+${index + 1}`;
|
|
190
208
|
return {
|
|
191
|
-
id: `${
|
|
209
|
+
id: `${scenario}-${index + 1}-${profileCommand.command}`,
|
|
192
210
|
scenario,
|
|
193
211
|
runId,
|
|
194
212
|
command: profileCommand.command,
|
|
@@ -196,8 +214,9 @@ function buildProfileSessionStorageWrites({ commands, commandStorageKey, command
|
|
|
196
214
|
...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
|
|
197
215
|
...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
|
|
198
216
|
...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
|
|
217
|
+
...(typeof profileCommand.waitMs === 'number' ? { waitMs: profileCommand.waitMs } : {}),
|
|
199
218
|
...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
|
|
200
|
-
timestamp,
|
|
219
|
+
timestamp: timestampPlaceholder,
|
|
201
220
|
};
|
|
202
221
|
});
|
|
203
222
|
const commandWaitMsTotal = commands.reduce((total, profileCommand) => (total + (typeof profileCommand.waitMs === 'number' && profileCommand.waitMs > 0 ? profileCommand.waitMs : 0)), 0);
|
|
@@ -274,6 +293,32 @@ function resolveProfileSessionCaptureWaitMs({ args, profileSessionEnabled, scena
|
|
|
274
293
|
}
|
|
275
294
|
return profileSessionEnabled ? deriveProfileSessionCaptureWaitMs(scenario) : 0;
|
|
276
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Derives a bounded logcat tail large enough to keep command-session evidence.
|
|
298
|
+
*
|
|
299
|
+
* @param {{commands: AndroidAdbProfileCommand[], profileSessionEnabled: boolean}} options
|
|
300
|
+
* @returns {number}
|
|
301
|
+
*/
|
|
302
|
+
function deriveProfileSessionLogcatLines({ commands, profileSessionEnabled, }) {
|
|
303
|
+
if (!profileSessionEnabled || commands.length === 0) {
|
|
304
|
+
return PROFILE_SESSION_LOGCAT_MIN_LINES;
|
|
305
|
+
}
|
|
306
|
+
const derivedLines = PROFILE_SESSION_LOGCAT_MIN_LINES + (commands.length * PROFILE_SESSION_LOGCAT_LINES_PER_COMMAND);
|
|
307
|
+
return Math.min(Math.max(derivedLines, PROFILE_SESSION_LOGCAT_MIN_LINES), PROFILE_SESSION_LOGCAT_MAX_LINES);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Resolves Android logcat capture lines, keeping explicit CLI input authoritative.
|
|
311
|
+
*
|
|
312
|
+
* @param {{args: import('./profile-mobile').CliArgs, commands: AndroidAdbProfileCommand[], profileSessionEnabled: boolean}} options
|
|
313
|
+
* @returns {number}
|
|
314
|
+
*/
|
|
315
|
+
function resolveProfileSessionLogcatLines({ args, commands, profileSessionEnabled, }) {
|
|
316
|
+
const explicitLogcatLines = readScalarArg(args['logcat-lines']);
|
|
317
|
+
if (explicitLogcatLines !== undefined) {
|
|
318
|
+
return parsePositiveInteger(explicitLogcatLines, PROFILE_SESSION_LOGCAT_MIN_LINES);
|
|
319
|
+
}
|
|
320
|
+
return deriveProfileSessionLogcatLines({ commands, profileSessionEnabled });
|
|
321
|
+
}
|
|
277
322
|
/**
|
|
278
323
|
* Reads Android adb adapter metadata from a normalized scenario step.
|
|
279
324
|
*
|
|
@@ -341,16 +386,182 @@ function resolveExecutionPlanProfileCommands(scenario) {
|
|
|
341
386
|
waitMs: readStepWaitMs(step),
|
|
342
387
|
...(nextStep?.portMethod === 'waitForTruthEvent' && typeof nextStep.milestone === 'string'
|
|
343
388
|
? {
|
|
344
|
-
waitForMilestone: nextStep.milestone,
|
|
389
|
+
waitForMilestone: resolveMilestoneEventName(scenario, nextStep.milestone),
|
|
345
390
|
waitTimeoutMs: readPositiveInteger(nextStep.timeoutMs, 0),
|
|
346
391
|
}
|
|
347
392
|
: {}),
|
|
348
393
|
});
|
|
349
394
|
}
|
|
350
|
-
return
|
|
395
|
+
return expandProfileCommandCycles(scenario, commands, repeat);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Returns true when a command is part of the setup prefix that establishes app readiness before repeated cycle work.
|
|
399
|
+
*
|
|
400
|
+
* @param {Record<string, unknown>} scenario
|
|
401
|
+
* @param {AndroidAdbProfileCommand} command
|
|
402
|
+
* @returns {boolean}
|
|
403
|
+
*/
|
|
404
|
+
function isReadinessSetupProfileCommand(scenario, command) {
|
|
405
|
+
if (typeof command.waitForMilestone !== 'string') {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
const readyEvent = resolveScenarioReadinessEvent(scenario);
|
|
409
|
+
return typeof readyEvent === 'string' && command.waitForMilestone === readyEvent;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Reads a string id list from scenario cycles metadata.
|
|
413
|
+
*
|
|
414
|
+
* @param {unknown} value
|
|
415
|
+
* @returns {Set<string>}
|
|
416
|
+
*/
|
|
417
|
+
function readCycleStepIdSet(value) {
|
|
418
|
+
return new Set(Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : []);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Resolves the milestone ids that represent measured cycle boundaries.
|
|
422
|
+
*
|
|
423
|
+
* @param {Record<string, unknown>} scenario
|
|
424
|
+
* @returns {Set<string>}
|
|
425
|
+
*/
|
|
426
|
+
function resolveMeasuredCycleMilestoneEvents(scenario) {
|
|
427
|
+
const milestones = new Set();
|
|
428
|
+
for (const budget of Array.isArray(scenario.budgets) ? scenario.budgets : []) {
|
|
429
|
+
if (!budget || typeof budget !== 'object' || budget.source !== 'milestone') {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (typeof budget.fromMilestone === 'string') {
|
|
433
|
+
milestones.add(resolveMilestoneEventName(scenario, budget.fromMilestone));
|
|
434
|
+
}
|
|
435
|
+
if (typeof budget.toMilestone === 'string') {
|
|
436
|
+
milestones.add(resolveMilestoneEventName(scenario, budget.toMilestone));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return milestones;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Resolves how many leading commands are setup-only before repeated cycle work.
|
|
443
|
+
*
|
|
444
|
+
* @param {Record<string, unknown>} scenario
|
|
445
|
+
* @param {AndroidAdbProfileCommand[]} commands
|
|
446
|
+
* @returns {number}
|
|
447
|
+
*/
|
|
448
|
+
function resolveSetupCommandCount(scenario, commands) {
|
|
449
|
+
const explicitSetupStepIds = readCycleStepIdSet(scenario.cycles?.setupStepIds);
|
|
450
|
+
if (explicitSetupStepIds.size > 0) {
|
|
451
|
+
let count = 0;
|
|
452
|
+
for (const command of commands) {
|
|
453
|
+
if (!command.commandId || !explicitSetupStepIds.has(command.commandId)) {
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
count += 1;
|
|
457
|
+
}
|
|
458
|
+
return count;
|
|
459
|
+
}
|
|
460
|
+
const explicitBodyStepIds = readCycleStepIdSet(scenario.cycles?.bodyStepIds);
|
|
461
|
+
if (explicitBodyStepIds.size > 0) {
|
|
462
|
+
const firstBodyIndex = commands.findIndex((command) => (typeof command.commandId === 'string' && explicitBodyStepIds.has(command.commandId)));
|
|
463
|
+
return firstBodyIndex > 0 ? firstBodyIndex : 0;
|
|
464
|
+
}
|
|
465
|
+
let readinessSetupCommandCount = 0;
|
|
466
|
+
for (const command of commands) {
|
|
467
|
+
if (!isReadinessSetupProfileCommand(scenario, command)) {
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
readinessSetupCommandCount += 1;
|
|
471
|
+
}
|
|
472
|
+
if (readinessSetupCommandCount > 0) {
|
|
473
|
+
return readinessSetupCommandCount;
|
|
474
|
+
}
|
|
475
|
+
const measuredMilestones = resolveMeasuredCycleMilestoneEvents(scenario);
|
|
476
|
+
if (measuredMilestones.size === 0) {
|
|
477
|
+
return 0;
|
|
478
|
+
}
|
|
479
|
+
const firstMeasuredCommandIndex = commands.findIndex((command) => (typeof command.waitForMilestone === 'string' && measuredMilestones.has(command.waitForMilestone)));
|
|
480
|
+
return firstMeasuredCommandIndex > 0 ? firstMeasuredCommandIndex : 0;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Expands commands so setup/readiness commands execute once while cycle-body commands repeat.
|
|
484
|
+
*
|
|
485
|
+
* @param {Record<string, unknown>} scenario
|
|
486
|
+
* @param {AndroidAdbProfileCommand[]} commands
|
|
487
|
+
* @param {number} repeat
|
|
488
|
+
* @returns {AndroidAdbProfileCommand[]}
|
|
489
|
+
*/
|
|
490
|
+
function expandProfileCommandCycles(scenario, commands, repeat) {
|
|
491
|
+
const setupCommandCount = resolveSetupCommandCount(scenario, commands);
|
|
492
|
+
const setupCommands = commands.slice(0, setupCommandCount);
|
|
493
|
+
const cycleCommands = commands.slice(setupCommandCount);
|
|
494
|
+
const expandedCommands = cycleCommands.length === 0
|
|
495
|
+
? setupCommands
|
|
496
|
+
: [
|
|
497
|
+
...setupCommands,
|
|
498
|
+
...Array.from({ length: repeat }).flatMap(() => cycleCommands),
|
|
499
|
+
];
|
|
500
|
+
return expandedCommands.map((command, index) => ({
|
|
351
501
|
...command,
|
|
352
|
-
sequence:
|
|
353
|
-
}))
|
|
502
|
+
sequence: index + 1,
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Resolves a portable milestone id to the app truth event that releases command sequencing.
|
|
507
|
+
*
|
|
508
|
+
* @param {Record<string, unknown>} scenario
|
|
509
|
+
* @param {string} milestone
|
|
510
|
+
* @returns {string}
|
|
511
|
+
*/
|
|
512
|
+
function resolveMilestoneEventName(scenario, milestone) {
|
|
513
|
+
const milestoneEntry = Array.isArray(scenario.milestones)
|
|
514
|
+
? scenario.milestones.find((entry) => entry?.id === milestone)
|
|
515
|
+
: undefined;
|
|
516
|
+
if (typeof milestoneEntry?.event === 'string' && milestoneEntry.event.length > 0) {
|
|
517
|
+
return milestoneEntry.event;
|
|
518
|
+
}
|
|
519
|
+
const metricEvent = scenario.metricEvents?.[milestone];
|
|
520
|
+
return typeof metricEvent === 'string' && metricEvent.length > 0 ? metricEvent : milestone;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Resolves the scenario truth event that represents initial app readiness.
|
|
524
|
+
*
|
|
525
|
+
* @param {Record<string, unknown>} scenario
|
|
526
|
+
* @returns {string | null}
|
|
527
|
+
*/
|
|
528
|
+
function resolveScenarioReadinessEvent(scenario) {
|
|
529
|
+
const explicitReadyEvent = scenario.truthEvents?.ready?.event;
|
|
530
|
+
if (typeof explicitReadyEvent === 'string' && explicitReadyEvent.length > 0) {
|
|
531
|
+
return explicitReadyEvent;
|
|
532
|
+
}
|
|
533
|
+
const milestoneEntry = Array.isArray(scenario.milestones)
|
|
534
|
+
? scenario.milestones.find((entry) => (String(entry?.event ?? '').includes('ready')))
|
|
535
|
+
: undefined;
|
|
536
|
+
return typeof milestoneEntry?.event === 'string' && milestoneEntry.event.length > 0
|
|
537
|
+
? milestoneEntry.event
|
|
538
|
+
: null;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Applies wait gates from the normalized execution plan to platform-declared commands.
|
|
542
|
+
*
|
|
543
|
+
* @param {Record<string, unknown>} scenario
|
|
544
|
+
* @param {AndroidAdbProfileCommand[]} commands
|
|
545
|
+
* @returns {AndroidAdbProfileCommand[]}
|
|
546
|
+
*/
|
|
547
|
+
function applyExecutionPlanCommandGates(scenario, commands) {
|
|
548
|
+
const planCommands = resolveExecutionPlanProfileCommands(scenario);
|
|
549
|
+
if (planCommands.length === 0) {
|
|
550
|
+
return commands;
|
|
551
|
+
}
|
|
552
|
+
return commands.map((command, index) => {
|
|
553
|
+
const planCommand = planCommands[index];
|
|
554
|
+
if (!planCommand || typeof planCommand.waitForMilestone !== 'string' || typeof command.waitForMilestone === 'string') {
|
|
555
|
+
return command;
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
...command,
|
|
559
|
+
waitForMilestone: planCommand.waitForMilestone,
|
|
560
|
+
...(typeof command.waitTimeoutMs === 'number'
|
|
561
|
+
? {}
|
|
562
|
+
: { waitTimeoutMs: readPositiveInteger(planCommand.waitTimeoutMs, 0) }),
|
|
563
|
+
};
|
|
564
|
+
});
|
|
354
565
|
}
|
|
355
566
|
/**
|
|
356
567
|
* Expands normalized scenario evidence steps into Android adb driver actions.
|
|
@@ -478,7 +689,7 @@ function resolveAndroidAdbProfileCommands(scenario) {
|
|
|
478
689
|
});
|
|
479
690
|
}
|
|
480
691
|
}
|
|
481
|
-
return commands;
|
|
692
|
+
return applyExecutionPlanCommandGates(scenario, commands);
|
|
482
693
|
}
|
|
483
694
|
/**
|
|
484
695
|
* Summarizes failed adb capture checks for CLI errors.
|
|
@@ -553,8 +764,32 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
553
764
|
const config = readJson(path.resolve(args.config));
|
|
554
765
|
const scenario = readJson(path.resolve(args.scenario));
|
|
555
766
|
const runId = typeof args['run-id'] === 'string' ? args['run-id'] : createRunId();
|
|
767
|
+
const scenarioName = resolveProfileScenarioName({ scenario, scenarioPath: path.resolve(args.scenario) });
|
|
768
|
+
const artifactRoot = resolveArtifactRoot({
|
|
769
|
+
args,
|
|
770
|
+
config,
|
|
771
|
+
configPath: path.resolve(args.config),
|
|
772
|
+
platform: 'android',
|
|
773
|
+
});
|
|
556
774
|
const adbCaptureEnabled = isEnabled(args['adb-capture']);
|
|
557
775
|
const agentDeviceCaptureEnabled = isEnabled(args['agent-device-capture']);
|
|
776
|
+
const driverSteps = adbCaptureEnabled ? resolveAndroidAdbDriverSteps(scenario) : [];
|
|
777
|
+
if (adbCaptureEnabled) {
|
|
778
|
+
const driverStepErrors = validateAndroidAdbDriverSteps(driverSteps);
|
|
779
|
+
if (driverStepErrors.length > 0) {
|
|
780
|
+
throw new Error(`Invalid Android adb driver step metadata: ${driverStepErrors.join(' ')}`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
await runProfileCompatibilityPreflight({
|
|
784
|
+
args,
|
|
785
|
+
artifactRoot,
|
|
786
|
+
platform: 'android',
|
|
787
|
+
primaryRunner: ANDROID_PROFILE_RUNNER_CAPABILITIES,
|
|
788
|
+
runDir: path.join(artifactRoot, scenarioName, runId),
|
|
789
|
+
runId,
|
|
790
|
+
scenario,
|
|
791
|
+
scenarioName,
|
|
792
|
+
});
|
|
558
793
|
const profileSessionEnabled = isEnabled(args['profile-session']);
|
|
559
794
|
const profileSessionStorageEnabled = isEnabled(args['android-profile-session-storage']);
|
|
560
795
|
const profileSessionStorageKey = readStringArgOrEnv(args['android-profile-session-storage-key'], [
|
|
@@ -573,10 +808,14 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
573
808
|
'ASL_ANDROID_DEV_CLIENT_WAIT_MS',
|
|
574
809
|
'ASL_EXAMPLE_ANDROID_DEV_CLIENT_WAIT_MS',
|
|
575
810
|
]), 1000);
|
|
576
|
-
const
|
|
811
|
+
const configuredAndroidDevClientReadyPattern = readStringArgOrEnv(args['android-dev-client-ready-pattern'], [
|
|
577
812
|
'ASL_ANDROID_DEV_CLIENT_READY_PATTERN',
|
|
578
813
|
'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_PATTERN',
|
|
579
814
|
]);
|
|
815
|
+
const androidDevClientReadyPattern = configuredAndroidDevClientReadyPattern
|
|
816
|
+
?? (androidDevClientUrl && profileSessionEnabled && profileSessionStorageEnabled
|
|
817
|
+
? DEFAULT_ANDROID_DEV_CLIENT_READY_PATTERN
|
|
818
|
+
: undefined);
|
|
580
819
|
const androidDevClientReadyQuietMs = parsePositiveInteger(readStringArgOrEnv(args['android-dev-client-ready-quiet-ms'], [
|
|
581
820
|
'ASL_ANDROID_DEV_CLIENT_READY_QUIET_MS',
|
|
582
821
|
'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_QUIET_MS',
|
|
@@ -585,14 +824,10 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
585
824
|
'ASL_ANDROID_DEV_CLIENT_READY_TIMEOUT_MS',
|
|
586
825
|
'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_TIMEOUT_MS',
|
|
587
826
|
]), 60000);
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
if (driverStepErrors.length > 0) {
|
|
593
|
-
throw new Error(`Invalid Android adb driver step metadata: ${driverStepErrors.join(' ')}`);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
827
|
+
const adbCommandTimeoutMs = parsePositiveInteger(readStringArgOrEnv(args['adb-command-timeout-ms'], [
|
|
828
|
+
'ASL_ANDROID_ADB_COMMAND_TIMEOUT_MS',
|
|
829
|
+
'ASL_EXAMPLE_ANDROID_ADB_COMMAND_TIMEOUT_MS',
|
|
830
|
+
]), 30000);
|
|
596
831
|
const profileSessionCommands = profileSessionEnabled ? resolveAndroidAdbProfileCommands(scenario) : [];
|
|
597
832
|
const commandWaitMs = parsePositiveInteger(readScalarArg(args['command-wait-ms']), 250);
|
|
598
833
|
const profileSessionDeepLinks = profileSessionEnabled && !profileSessionStorageEnabled
|
|
@@ -619,6 +854,7 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
619
854
|
...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
|
|
620
855
|
...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
|
|
621
856
|
...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
|
|
857
|
+
...(typeof profileCommand.waitMs === 'number' ? { waitMs: profileCommand.waitMs } : {}),
|
|
622
858
|
...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
|
|
623
859
|
}),
|
|
624
860
|
waitMs: profileCommand.waitMs,
|
|
@@ -652,13 +888,18 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
652
888
|
...(typeof args.adb === 'string' ? { adbPath: args.adb } : {}),
|
|
653
889
|
captureLogcat: true,
|
|
654
890
|
clearLogcat: isEnabled(args['clear-logcat']),
|
|
891
|
+
commandTimeoutMs: adbCommandTimeoutMs,
|
|
655
892
|
deepLinks: profileSessionDeepLinks,
|
|
656
893
|
...(options.delay ? { delay: options.delay } : {}),
|
|
657
894
|
...(options.executor ? { executor: options.executor } : {}),
|
|
658
895
|
driverSteps,
|
|
659
896
|
launch: isEnabled(args.launch),
|
|
660
897
|
launchWaitMs: parsePositiveInteger(readScalarArg(args['launch-wait-ms']), 0),
|
|
661
|
-
logcatLines:
|
|
898
|
+
logcatLines: resolveProfileSessionLogcatLines({
|
|
899
|
+
args,
|
|
900
|
+
commands: profileSessionCommands,
|
|
901
|
+
profileSessionEnabled,
|
|
902
|
+
}),
|
|
662
903
|
outputDir: resolveAdbCaptureOutputDir({ args, runId }),
|
|
663
904
|
packageName: resolveAndroidPackageName({ args, config }),
|
|
664
905
|
...(typeof args['react-native-debug-host'] === 'string'
|
|
@@ -724,7 +965,7 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
724
965
|
: baseProfileArgs;
|
|
725
966
|
const lifecyclePhase = resolveManifestLifecyclePhase(args);
|
|
726
967
|
const environmentSource = agentDeviceCapture ? 'agent-device' : 'adb';
|
|
727
|
-
const
|
|
968
|
+
const copiedAdbLogArtifact = adbCapture ? 'raw/adb-logcat.txt' : undefined;
|
|
728
969
|
return runProfileMobile(profileArgs, {
|
|
729
970
|
commandTransport: profileSessionStorageEnabled
|
|
730
971
|
? 'profile-session-storage'
|
|
@@ -740,13 +981,13 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
740
981
|
value: 'foreground',
|
|
741
982
|
evidence: 'asserted',
|
|
742
983
|
source: environmentSource,
|
|
743
|
-
artifact:
|
|
984
|
+
...(copiedAdbLogArtifact ? { artifact: copiedAdbLogArtifact } : {}),
|
|
744
985
|
},
|
|
745
986
|
lifecyclePhase: {
|
|
746
987
|
value: 'foreground',
|
|
747
988
|
evidence: 'asserted',
|
|
748
989
|
source: environmentSource,
|
|
749
|
-
artifact:
|
|
990
|
+
...(copiedAdbLogArtifact ? { artifact: copiedAdbLogArtifact } : {}),
|
|
750
991
|
},
|
|
751
992
|
},
|
|
752
993
|
environmentPreconditions: {
|
|
@@ -759,7 +1000,7 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
759
1000
|
value: lifecyclePhase,
|
|
760
1001
|
evidence: 'asserted',
|
|
761
1002
|
source: environmentSource,
|
|
762
|
-
artifact:
|
|
1003
|
+
...(copiedAdbLogArtifact ? { artifact: copiedAdbLogArtifact } : {}),
|
|
763
1004
|
},
|
|
764
1005
|
},
|
|
765
1006
|
interactionDriver: agentDeviceCapture ? 'agent-device' : 'adb-logcat',
|
|
@@ -49,7 +49,7 @@ declare function resolveIosConflictingBundleIds(config: Record<string, any>): st
|
|
|
49
49
|
* @param {{config: Record<string, unknown>, action: 'start' | 'command', scenario: string, runId: string, command?: string}} options
|
|
50
50
|
* @returns {string}
|
|
51
51
|
*/
|
|
52
|
-
declare function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitTimeoutMs, }: {
|
|
52
|
+
declare function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitMs, waitTimeoutMs, }: {
|
|
53
53
|
action: 'start' | 'command';
|
|
54
54
|
command?: string;
|
|
55
55
|
commandId?: string;
|
|
@@ -59,10 +59,11 @@ declare function buildProfileSessionUrl({ action, command, commandId, config, qu
|
|
|
59
59
|
scenario: string;
|
|
60
60
|
sequence?: number;
|
|
61
61
|
waitForMilestone?: string;
|
|
62
|
+
waitMs?: number;
|
|
62
63
|
waitTimeoutMs?: number;
|
|
63
64
|
}): string;
|
|
64
65
|
/**
|
|
65
|
-
* Derives a storage-backed profile capture window from scenario waits and cycles.
|
|
66
|
+
* Derives a storage-backed profile capture window from scenario waits, command gates, and cycles.
|
|
66
67
|
*
|
|
67
68
|
* @param {Record<string, unknown>} scenario
|
|
68
69
|
* @returns {number}
|