agent-scenario-loop 0.1.2 → 0.1.4
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 +9 -9
- package/app/profile-session.ts +352 -12
- package/dist/core/agent-summary.d.ts +3 -2
- package/dist/core/agent-summary.js +44 -2
- package/dist/core/artifact-contract.d.ts +28 -8
- package/dist/core/artifact-contract.js +676 -26
- package/dist/core/comparison.d.ts +57 -3
- package/dist/core/comparison.js +113 -1
- package/dist/core/planner.d.ts +32 -1
- package/dist/core/planner.js +144 -0
- package/dist/core/run-index.d.ts +4 -0
- package/dist/core/run-index.js +55 -1
- package/dist/core/schema-validator.d.ts +2 -0
- package/dist/core/schema-validator.js +2 -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/compare-latest.d.ts +8 -4
- package/dist/runner/compare-latest.js +24 -5
- package/dist/runner/example-android-live.d.ts +10 -1
- package/dist/runner/example-android-live.js +55 -0
- package/dist/runner/example-ios-live.d.ts +10 -1
- package/dist/runner/example-ios-live.js +55 -0
- package/dist/runner/ios-simctl.d.ts +6 -0
- package/dist/runner/ios-simctl.js +7 -0
- package/dist/runner/live-comparison.d.ts +2 -2
- package/dist/runner/live-comparison.js +2 -1
- package/dist/runner/live-proof-summary.d.ts +5 -4
- package/dist/runner/live-proof-summary.js +12 -2
- package/dist/runner/live-proof.d.ts +3 -2
- package/dist/runner/live-proof.js +9 -2
- package/dist/runner/profile-android.d.ts +16 -1
- package/dist/runner/profile-android.js +364 -26
- package/dist/runner/profile-ios.d.ts +13 -2
- package/dist/runner/profile-ios.js +341 -19
- package/dist/runner/profile-mobile.d.ts +39 -3
- package/dist/runner/profile-mobile.js +1054 -42
- package/dist/runner/validate-project.js +3 -0
- package/dist/scripts/consumer-rehearsal.d.ts +119 -0
- package/dist/scripts/consumer-rehearsal.js +757 -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 +96 -0
- package/dist/scripts/package-smoke.js +2282 -0
- package/dist/scripts/release-readiness.d.ts +2 -0
- package/dist/scripts/release-readiness.js +520 -0
- package/docs/adapters.md +7 -1
- package/docs/api.md +2 -2
- package/docs/architecture.md +90 -0
- package/docs/authoring.md +39 -3
- package/docs/concepts.md +3 -24
- package/docs/consumer-rehearsal.md +31 -1
- package/docs/contracts.md +45 -101
- package/docs/external-adapter-protocol.md +219 -0
- package/docs/live-proofs.md +86 -3
- package/docs/principles.md +9 -15
- package/examples/mobile-app/README.md +12 -0
- package/examples/mobile-app/runner-manifests/evidence-provider.json +3 -3
- package/examples/mobile-app/runner-manifests/primary-runner.json +1 -0
- package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +25 -0
- package/examples/runners/README.md +4 -3
- package/examples/runners/adb-android.json +1 -0
- package/examples/runners/agent-device-android.json +1 -0
- package/examples/runners/agent-device-ios.json +1 -0
- package/examples/runners/argent-android.json +1 -0
- package/examples/runners/argent-ios.json +1 -0
- 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/examples/runners/xcodebuildmcp-ios.json +1 -0
- package/package.json +12 -3
- package/schemas/causal-run.schema.json +85 -2
- package/schemas/comparison.schema.json +130 -2
- package/schemas/external-adapter-message.schema.json +693 -0
- package/schemas/health.schema.json +72 -0
- package/schemas/live-proof-set.schema.json +1 -1
- package/schemas/live-proof.schema.json +14 -6
- package/schemas/manifest.schema.json +515 -4
- package/schemas/profiler.schema.json +243 -0
- package/schemas/runner-capabilities.schema.json +28 -2
- package/schemas/scenario.schema.json +34 -2
- package/templates/evidence-provider.json +3 -3
- package/templates/primary-runner.json +1 -0
- package/templates/scripts/asl-capture-profiler-provider.mjs +20 -0
|
@@ -8,8 +8,13 @@ type AndroidProfileOptions = {
|
|
|
8
8
|
};
|
|
9
9
|
type AndroidAdbProfileCommand = {
|
|
10
10
|
command: string;
|
|
11
|
+
commandId?: string;
|
|
11
12
|
label?: string;
|
|
13
|
+
queueId?: string;
|
|
14
|
+
sequence?: number;
|
|
15
|
+
waitForMilestone?: string;
|
|
12
16
|
waitMs?: number;
|
|
17
|
+
waitTimeoutMs?: number;
|
|
13
18
|
};
|
|
14
19
|
type AndroidAdbDriverStep = import('./android-adb').AndroidAdbDriverStep;
|
|
15
20
|
/**
|
|
@@ -30,6 +35,16 @@ declare function resolveProfileSessionCaptureWaitMs({ args, profileSessionEnable
|
|
|
30
35
|
profileSessionEnabled: boolean;
|
|
31
36
|
scenario: Record<string, any>;
|
|
32
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;
|
|
33
48
|
/**
|
|
34
49
|
* Expands normalized scenario evidence steps into Android adb driver actions.
|
|
35
50
|
*
|
|
@@ -79,4 +94,4 @@ declare function runProfileAndroid(args: import('./profile-mobile').CliArgs, opt
|
|
|
79
94
|
* @returns {Promise<void>}
|
|
80
95
|
*/
|
|
81
96
|
declare function main(): Promise<void>;
|
|
82
|
-
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;
|
|
@@ -24,8 +25,27 @@ 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"';
|
|
34
|
+
const MANIFEST_LIFECYCLE_PHASES = new Set([
|
|
35
|
+
'cold-launch',
|
|
36
|
+
'warm-launch',
|
|
37
|
+
'hot-launch',
|
|
38
|
+
'resume',
|
|
39
|
+
'foreground',
|
|
40
|
+
'background',
|
|
41
|
+
'force-stop',
|
|
42
|
+
'process-death',
|
|
43
|
+
'scene-recreation',
|
|
44
|
+
'activity-recreation',
|
|
45
|
+
'os-reclaim',
|
|
46
|
+
'reboot',
|
|
47
|
+
'relaunch',
|
|
48
|
+
]);
|
|
29
49
|
/**
|
|
30
50
|
* Reads and parses a JSON object from disk.
|
|
31
51
|
*
|
|
@@ -54,6 +74,22 @@ function isEnabled(value) {
|
|
|
54
74
|
function readPositiveInteger(value, fallback) {
|
|
55
75
|
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
56
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolves the lifecycle phase this runner is prepared to assert.
|
|
79
|
+
*
|
|
80
|
+
* @param {import('./profile-mobile').CliArgs} args
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function resolveManifestLifecyclePhase(args) {
|
|
84
|
+
const lifecyclePhase = readScalarArg(args['lifecycle-phase']);
|
|
85
|
+
if (lifecyclePhase === undefined) {
|
|
86
|
+
return 'cold-launch';
|
|
87
|
+
}
|
|
88
|
+
if (typeof lifecyclePhase !== 'string' || !MANIFEST_LIFECYCLE_PHASES.has(lifecyclePhase)) {
|
|
89
|
+
throw new Error(`Unsupported --lifecycle-phase "${String(lifecyclePhase)}". Expected one of ${Array.from(MANIFEST_LIFECYCLE_PHASES).join(', ')}.`);
|
|
90
|
+
}
|
|
91
|
+
return lifecyclePhase;
|
|
92
|
+
}
|
|
57
93
|
/**
|
|
58
94
|
* Reads the number of scenario iterations that can emit app-owned truth events.
|
|
59
95
|
*
|
|
@@ -119,7 +155,7 @@ function resolveAndroidPackageName({ args, config, }) {
|
|
|
119
155
|
* @param {{config: Record<string, unknown>, action: 'start' | 'command', scenario: string, runId: string, command?: string}} options
|
|
120
156
|
* @returns {string}
|
|
121
157
|
*/
|
|
122
|
-
function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
|
|
158
|
+
function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitMs, waitTimeoutMs, }) {
|
|
123
159
|
const scheme = typeof config.app?.profileSessionScheme === 'string'
|
|
124
160
|
? config.app.profileSessionScheme
|
|
125
161
|
: typeof config.app?.scheme === 'string'
|
|
@@ -128,6 +164,24 @@ function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
|
|
|
128
164
|
const params = new URLSearchParams({ runId, scenario });
|
|
129
165
|
if (action === 'command' && command) {
|
|
130
166
|
params.set('command', command);
|
|
167
|
+
if (commandId) {
|
|
168
|
+
params.set('commandId', commandId);
|
|
169
|
+
}
|
|
170
|
+
if (typeof sequence === 'number') {
|
|
171
|
+
params.set('sequence', String(sequence));
|
|
172
|
+
}
|
|
173
|
+
if (queueId) {
|
|
174
|
+
params.set('queueId', queueId);
|
|
175
|
+
}
|
|
176
|
+
if (waitForMilestone) {
|
|
177
|
+
params.set('waitForMilestone', waitForMilestone);
|
|
178
|
+
}
|
|
179
|
+
if (typeof waitMs === 'number') {
|
|
180
|
+
params.set('waitMs', String(waitMs));
|
|
181
|
+
}
|
|
182
|
+
if (typeof waitTimeoutMs === 'number') {
|
|
183
|
+
params.set('waitTimeoutMs', String(waitTimeoutMs));
|
|
184
|
+
}
|
|
131
185
|
}
|
|
132
186
|
return `${scheme}://profile-session/${action}?${params.toString()}`;
|
|
133
187
|
}
|
|
@@ -138,6 +192,23 @@ function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
|
|
|
138
192
|
* @returns {import('./android-adb').AndroidAsyncStorageWrite[]}
|
|
139
193
|
*/
|
|
140
194
|
function buildProfileSessionStorageWrites({ commands, commandStorageKey, commandWaitMs, runId, scenario, sessionStorageKey, }) {
|
|
195
|
+
const storedCommands = commands.map((profileCommand, index) => {
|
|
196
|
+
const timestampPlaceholder = `${ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER}+${index + 1}`;
|
|
197
|
+
return {
|
|
198
|
+
id: `${scenario}-${index + 1}-${profileCommand.command}`,
|
|
199
|
+
scenario,
|
|
200
|
+
runId,
|
|
201
|
+
command: profileCommand.command,
|
|
202
|
+
...(typeof profileCommand.commandId === 'string' ? { commandId: profileCommand.commandId } : {}),
|
|
203
|
+
...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
|
|
204
|
+
...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
|
|
205
|
+
...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
|
|
206
|
+
...(typeof profileCommand.waitMs === 'number' ? { waitMs: profileCommand.waitMs } : {}),
|
|
207
|
+
...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
|
|
208
|
+
timestamp: timestampPlaceholder,
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
const commandWaitMsTotal = commands.reduce((total, profileCommand) => (total + (typeof profileCommand.waitMs === 'number' && profileCommand.waitMs > 0 ? profileCommand.waitMs : 0)), 0);
|
|
141
212
|
return [
|
|
142
213
|
{
|
|
143
214
|
clearKeys: [commandStorageKey],
|
|
@@ -151,21 +222,14 @@ function buildProfileSessionStorageWrites({ commands, commandStorageKey, command
|
|
|
151
222
|
}).replace(`"${ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER}"`, ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER),
|
|
152
223
|
waitMs: commandWaitMs,
|
|
153
224
|
},
|
|
154
|
-
...
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
runId,
|
|
163
|
-
command: profileCommand.command,
|
|
164
|
-
timestamp,
|
|
165
|
-
}]),
|
|
166
|
-
...(typeof profileCommand.waitMs === 'number' ? { waitMs: profileCommand.waitMs } : {}),
|
|
167
|
-
};
|
|
168
|
-
}),
|
|
225
|
+
...(storedCommands.length > 0
|
|
226
|
+
? [{
|
|
227
|
+
key: commandStorageKey,
|
|
228
|
+
label: 'profile-command-queue',
|
|
229
|
+
value: JSON.stringify(storedCommands),
|
|
230
|
+
...(commandWaitMsTotal > 0 ? { waitMs: commandWaitMsTotal } : {}),
|
|
231
|
+
}]
|
|
232
|
+
: []),
|
|
169
233
|
];
|
|
170
234
|
}
|
|
171
235
|
/**
|
|
@@ -218,6 +282,32 @@ function resolveProfileSessionCaptureWaitMs({ args, profileSessionEnabled, scena
|
|
|
218
282
|
}
|
|
219
283
|
return profileSessionEnabled ? deriveProfileSessionCaptureWaitMs(scenario) : 0;
|
|
220
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Derives a bounded logcat tail large enough to keep command-session evidence.
|
|
287
|
+
*
|
|
288
|
+
* @param {{commands: AndroidAdbProfileCommand[], profileSessionEnabled: boolean}} options
|
|
289
|
+
* @returns {number}
|
|
290
|
+
*/
|
|
291
|
+
function deriveProfileSessionLogcatLines({ commands, profileSessionEnabled, }) {
|
|
292
|
+
if (!profileSessionEnabled || commands.length === 0) {
|
|
293
|
+
return PROFILE_SESSION_LOGCAT_MIN_LINES;
|
|
294
|
+
}
|
|
295
|
+
const derivedLines = PROFILE_SESSION_LOGCAT_MIN_LINES + (commands.length * PROFILE_SESSION_LOGCAT_LINES_PER_COMMAND);
|
|
296
|
+
return Math.min(Math.max(derivedLines, PROFILE_SESSION_LOGCAT_MIN_LINES), PROFILE_SESSION_LOGCAT_MAX_LINES);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Resolves Android logcat capture lines, keeping explicit CLI input authoritative.
|
|
300
|
+
*
|
|
301
|
+
* @param {{args: import('./profile-mobile').CliArgs, commands: AndroidAdbProfileCommand[], profileSessionEnabled: boolean}} options
|
|
302
|
+
* @returns {number}
|
|
303
|
+
*/
|
|
304
|
+
function resolveProfileSessionLogcatLines({ args, commands, profileSessionEnabled, }) {
|
|
305
|
+
const explicitLogcatLines = readScalarArg(args['logcat-lines']);
|
|
306
|
+
if (explicitLogcatLines !== undefined) {
|
|
307
|
+
return parsePositiveInteger(explicitLogcatLines, PROFILE_SESSION_LOGCAT_MIN_LINES);
|
|
308
|
+
}
|
|
309
|
+
return deriveProfileSessionLogcatLines({ commands, profileSessionEnabled });
|
|
310
|
+
}
|
|
221
311
|
/**
|
|
222
312
|
* Reads Android adb adapter metadata from a normalized scenario step.
|
|
223
313
|
*
|
|
@@ -271,14 +361,196 @@ function appendCaptureArg({ args, value, }) {
|
|
|
271
361
|
function resolveExecutionPlanProfileCommands(scenario) {
|
|
272
362
|
const executionPlan = buildScenarioExecutionPlan(scenario);
|
|
273
363
|
const repeat = readPositiveInteger(scenario.defaultIterations, readPositiveInteger(scenario.cycles?.iterations, 1));
|
|
274
|
-
const commands =
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
364
|
+
const commands = [];
|
|
365
|
+
for (const [index, step] of executionPlan.steps.entries()) {
|
|
366
|
+
if (step.portMethod !== 'executeStep' || typeof step.command !== 'string') {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const nextStep = executionPlan.steps[index + 1];
|
|
370
|
+
commands.push({
|
|
371
|
+
command: step.command,
|
|
372
|
+
commandId: step.id,
|
|
373
|
+
label: step.id,
|
|
374
|
+
queueId: scenario.id ?? scenario.name,
|
|
375
|
+
waitMs: readStepWaitMs(step),
|
|
376
|
+
...(nextStep?.portMethod === 'waitForTruthEvent' && typeof nextStep.milestone === 'string'
|
|
377
|
+
? {
|
|
378
|
+
waitForMilestone: resolveMilestoneEventName(scenario, nextStep.milestone),
|
|
379
|
+
waitTimeoutMs: readPositiveInteger(nextStep.timeoutMs, 0),
|
|
380
|
+
}
|
|
381
|
+
: {}),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return expandProfileCommandCycles(scenario, commands, repeat);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Returns true when a command is part of the setup prefix that establishes app readiness before repeated cycle work.
|
|
388
|
+
*
|
|
389
|
+
* @param {Record<string, unknown>} scenario
|
|
390
|
+
* @param {AndroidAdbProfileCommand} command
|
|
391
|
+
* @returns {boolean}
|
|
392
|
+
*/
|
|
393
|
+
function isReadinessSetupProfileCommand(scenario, command) {
|
|
394
|
+
if (typeof command.waitForMilestone !== 'string') {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
const readyEvent = resolveScenarioReadinessEvent(scenario);
|
|
398
|
+
return typeof readyEvent === 'string' && command.waitForMilestone === readyEvent;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Reads a string id list from scenario cycles metadata.
|
|
402
|
+
*
|
|
403
|
+
* @param {unknown} value
|
|
404
|
+
* @returns {Set<string>}
|
|
405
|
+
*/
|
|
406
|
+
function readCycleStepIdSet(value) {
|
|
407
|
+
return new Set(Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : []);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Resolves the milestone ids that represent measured cycle boundaries.
|
|
411
|
+
*
|
|
412
|
+
* @param {Record<string, unknown>} scenario
|
|
413
|
+
* @returns {Set<string>}
|
|
414
|
+
*/
|
|
415
|
+
function resolveMeasuredCycleMilestoneEvents(scenario) {
|
|
416
|
+
const milestones = new Set();
|
|
417
|
+
for (const budget of Array.isArray(scenario.budgets) ? scenario.budgets : []) {
|
|
418
|
+
if (!budget || typeof budget !== 'object' || budget.source !== 'milestone') {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (typeof budget.fromMilestone === 'string') {
|
|
422
|
+
milestones.add(resolveMilestoneEventName(scenario, budget.fromMilestone));
|
|
423
|
+
}
|
|
424
|
+
if (typeof budget.toMilestone === 'string') {
|
|
425
|
+
milestones.add(resolveMilestoneEventName(scenario, budget.toMilestone));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return milestones;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Resolves how many leading commands are setup-only before repeated cycle work.
|
|
432
|
+
*
|
|
433
|
+
* @param {Record<string, unknown>} scenario
|
|
434
|
+
* @param {AndroidAdbProfileCommand[]} commands
|
|
435
|
+
* @returns {number}
|
|
436
|
+
*/
|
|
437
|
+
function resolveSetupCommandCount(scenario, commands) {
|
|
438
|
+
const explicitSetupStepIds = readCycleStepIdSet(scenario.cycles?.setupStepIds);
|
|
439
|
+
if (explicitSetupStepIds.size > 0) {
|
|
440
|
+
let count = 0;
|
|
441
|
+
for (const command of commands) {
|
|
442
|
+
if (!command.commandId || !explicitSetupStepIds.has(command.commandId)) {
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
count += 1;
|
|
446
|
+
}
|
|
447
|
+
return count;
|
|
448
|
+
}
|
|
449
|
+
const explicitBodyStepIds = readCycleStepIdSet(scenario.cycles?.bodyStepIds);
|
|
450
|
+
if (explicitBodyStepIds.size > 0) {
|
|
451
|
+
const firstBodyIndex = commands.findIndex((command) => (typeof command.commandId === 'string' && explicitBodyStepIds.has(command.commandId)));
|
|
452
|
+
return firstBodyIndex > 0 ? firstBodyIndex : 0;
|
|
453
|
+
}
|
|
454
|
+
let readinessSetupCommandCount = 0;
|
|
455
|
+
for (const command of commands) {
|
|
456
|
+
if (!isReadinessSetupProfileCommand(scenario, command)) {
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
readinessSetupCommandCount += 1;
|
|
460
|
+
}
|
|
461
|
+
if (readinessSetupCommandCount > 0) {
|
|
462
|
+
return readinessSetupCommandCount;
|
|
463
|
+
}
|
|
464
|
+
const measuredMilestones = resolveMeasuredCycleMilestoneEvents(scenario);
|
|
465
|
+
if (measuredMilestones.size === 0) {
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
const firstMeasuredCommandIndex = commands.findIndex((command) => (typeof command.waitForMilestone === 'string' && measuredMilestones.has(command.waitForMilestone)));
|
|
469
|
+
return firstMeasuredCommandIndex > 0 ? firstMeasuredCommandIndex : 0;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Expands commands so setup/readiness commands execute once while cycle-body commands repeat.
|
|
473
|
+
*
|
|
474
|
+
* @param {Record<string, unknown>} scenario
|
|
475
|
+
* @param {AndroidAdbProfileCommand[]} commands
|
|
476
|
+
* @param {number} repeat
|
|
477
|
+
* @returns {AndroidAdbProfileCommand[]}
|
|
478
|
+
*/
|
|
479
|
+
function expandProfileCommandCycles(scenario, commands, repeat) {
|
|
480
|
+
const setupCommandCount = resolveSetupCommandCount(scenario, commands);
|
|
481
|
+
const setupCommands = commands.slice(0, setupCommandCount);
|
|
482
|
+
const cycleCommands = commands.slice(setupCommandCount);
|
|
483
|
+
const expandedCommands = cycleCommands.length === 0
|
|
484
|
+
? setupCommands
|
|
485
|
+
: [
|
|
486
|
+
...setupCommands,
|
|
487
|
+
...Array.from({ length: repeat }).flatMap(() => cycleCommands),
|
|
488
|
+
];
|
|
489
|
+
return expandedCommands.map((command, index) => ({
|
|
490
|
+
...command,
|
|
491
|
+
sequence: index + 1,
|
|
280
492
|
}));
|
|
281
|
-
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Resolves a portable milestone id to the app truth event that releases command sequencing.
|
|
496
|
+
*
|
|
497
|
+
* @param {Record<string, unknown>} scenario
|
|
498
|
+
* @param {string} milestone
|
|
499
|
+
* @returns {string}
|
|
500
|
+
*/
|
|
501
|
+
function resolveMilestoneEventName(scenario, milestone) {
|
|
502
|
+
const milestoneEntry = Array.isArray(scenario.milestones)
|
|
503
|
+
? scenario.milestones.find((entry) => entry?.id === milestone)
|
|
504
|
+
: undefined;
|
|
505
|
+
if (typeof milestoneEntry?.event === 'string' && milestoneEntry.event.length > 0) {
|
|
506
|
+
return milestoneEntry.event;
|
|
507
|
+
}
|
|
508
|
+
const metricEvent = scenario.metricEvents?.[milestone];
|
|
509
|
+
return typeof metricEvent === 'string' && metricEvent.length > 0 ? metricEvent : milestone;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Resolves the scenario truth event that represents initial app readiness.
|
|
513
|
+
*
|
|
514
|
+
* @param {Record<string, unknown>} scenario
|
|
515
|
+
* @returns {string | null}
|
|
516
|
+
*/
|
|
517
|
+
function resolveScenarioReadinessEvent(scenario) {
|
|
518
|
+
const explicitReadyEvent = scenario.truthEvents?.ready?.event;
|
|
519
|
+
if (typeof explicitReadyEvent === 'string' && explicitReadyEvent.length > 0) {
|
|
520
|
+
return explicitReadyEvent;
|
|
521
|
+
}
|
|
522
|
+
const milestoneEntry = Array.isArray(scenario.milestones)
|
|
523
|
+
? scenario.milestones.find((entry) => (String(entry?.event ?? '').includes('ready')))
|
|
524
|
+
: undefined;
|
|
525
|
+
return typeof milestoneEntry?.event === 'string' && milestoneEntry.event.length > 0
|
|
526
|
+
? milestoneEntry.event
|
|
527
|
+
: null;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Applies wait gates from the normalized execution plan to platform-declared commands.
|
|
531
|
+
*
|
|
532
|
+
* @param {Record<string, unknown>} scenario
|
|
533
|
+
* @param {AndroidAdbProfileCommand[]} commands
|
|
534
|
+
* @returns {AndroidAdbProfileCommand[]}
|
|
535
|
+
*/
|
|
536
|
+
function applyExecutionPlanCommandGates(scenario, commands) {
|
|
537
|
+
const planCommands = resolveExecutionPlanProfileCommands(scenario);
|
|
538
|
+
if (planCommands.length === 0) {
|
|
539
|
+
return commands;
|
|
540
|
+
}
|
|
541
|
+
return commands.map((command, index) => {
|
|
542
|
+
const planCommand = planCommands[index];
|
|
543
|
+
if (!planCommand || typeof planCommand.waitForMilestone !== 'string' || typeof command.waitForMilestone === 'string') {
|
|
544
|
+
return command;
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
...command,
|
|
548
|
+
waitForMilestone: planCommand.waitForMilestone,
|
|
549
|
+
...(typeof command.waitTimeoutMs === 'number'
|
|
550
|
+
? {}
|
|
551
|
+
: { waitTimeoutMs: readPositiveInteger(planCommand.waitTimeoutMs, 0) }),
|
|
552
|
+
};
|
|
553
|
+
});
|
|
282
554
|
}
|
|
283
555
|
/**
|
|
284
556
|
* Expands normalized scenario evidence steps into Android adb driver actions.
|
|
@@ -392,12 +664,21 @@ function resolveAndroidAdbProfileCommands(scenario) {
|
|
|
392
664
|
}
|
|
393
665
|
commands.push({
|
|
394
666
|
command: command.command,
|
|
667
|
+
commandId: typeof command.id === 'string'
|
|
668
|
+
? command.id
|
|
669
|
+
: typeof command.commandId === 'string'
|
|
670
|
+
? command.commandId
|
|
671
|
+
: typeof command.label === 'string'
|
|
672
|
+
? command.label
|
|
673
|
+
: command.command,
|
|
395
674
|
...(typeof command.label === 'string' ? { label: command.label } : {}),
|
|
675
|
+
queueId: scenario.id ?? scenario.name,
|
|
676
|
+
sequence: commands.length + 1,
|
|
396
677
|
waitMs: readPositiveInteger(command.waitMs, 0),
|
|
397
678
|
});
|
|
398
679
|
}
|
|
399
680
|
}
|
|
400
|
-
return commands;
|
|
681
|
+
return applyExecutionPlanCommandGates(scenario, commands);
|
|
401
682
|
}
|
|
402
683
|
/**
|
|
403
684
|
* Summarizes failed adb capture checks for CLI errors.
|
|
@@ -459,6 +740,7 @@ function appendAgentDeviceCaptureArgs({ args, capture, }) {
|
|
|
459
740
|
async function runProfileAndroid(args, options = {}) {
|
|
460
741
|
if (!isEnabled(args['adb-capture']) && !isEnabled(args['agent-device-capture'])) {
|
|
461
742
|
return runProfileMobile(args, {
|
|
743
|
+
commandTransport: typeof args.events === 'string' ? 'fixture-log-ingest' : 'adb-artifacts',
|
|
462
744
|
...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
|
|
463
745
|
defaultDriver: 'adb-logcat',
|
|
464
746
|
...(typeof args['adb-artifacts'] === 'string' ? { interactionDriver: 'adb-logcat' } : {}),
|
|
@@ -491,10 +773,14 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
491
773
|
'ASL_ANDROID_DEV_CLIENT_WAIT_MS',
|
|
492
774
|
'ASL_EXAMPLE_ANDROID_DEV_CLIENT_WAIT_MS',
|
|
493
775
|
]), 1000);
|
|
494
|
-
const
|
|
776
|
+
const configuredAndroidDevClientReadyPattern = readStringArgOrEnv(args['android-dev-client-ready-pattern'], [
|
|
495
777
|
'ASL_ANDROID_DEV_CLIENT_READY_PATTERN',
|
|
496
778
|
'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_PATTERN',
|
|
497
779
|
]);
|
|
780
|
+
const androidDevClientReadyPattern = configuredAndroidDevClientReadyPattern
|
|
781
|
+
?? (androidDevClientUrl && profileSessionEnabled && profileSessionStorageEnabled
|
|
782
|
+
? DEFAULT_ANDROID_DEV_CLIENT_READY_PATTERN
|
|
783
|
+
: undefined);
|
|
498
784
|
const androidDevClientReadyQuietMs = parsePositiveInteger(readStringArgOrEnv(args['android-dev-client-ready-quiet-ms'], [
|
|
499
785
|
'ASL_ANDROID_DEV_CLIENT_READY_QUIET_MS',
|
|
500
786
|
'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_QUIET_MS',
|
|
@@ -503,6 +789,10 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
503
789
|
'ASL_ANDROID_DEV_CLIENT_READY_TIMEOUT_MS',
|
|
504
790
|
'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_TIMEOUT_MS',
|
|
505
791
|
]), 60000);
|
|
792
|
+
const adbCommandTimeoutMs = parsePositiveInteger(readStringArgOrEnv(args['adb-command-timeout-ms'], [
|
|
793
|
+
'ASL_ANDROID_ADB_COMMAND_TIMEOUT_MS',
|
|
794
|
+
'ASL_EXAMPLE_ANDROID_ADB_COMMAND_TIMEOUT_MS',
|
|
795
|
+
]), 30000);
|
|
506
796
|
const scenarioName = typeof scenario.name === 'string' ? scenario.name : path.basename(args.scenario, '.json');
|
|
507
797
|
const driverSteps = adbCaptureEnabled ? resolveAndroidAdbDriverSteps(scenario) : [];
|
|
508
798
|
if (adbCaptureEnabled) {
|
|
@@ -530,9 +820,15 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
530
820
|
url: buildProfileSessionUrl({
|
|
531
821
|
action: 'command',
|
|
532
822
|
command: profileCommand.command,
|
|
823
|
+
...(typeof profileCommand.commandId === 'string' ? { commandId: profileCommand.commandId } : {}),
|
|
533
824
|
config,
|
|
534
825
|
runId,
|
|
535
826
|
scenario: scenarioName,
|
|
827
|
+
...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
|
|
828
|
+
...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
|
|
829
|
+
...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
|
|
830
|
+
...(typeof profileCommand.waitMs === 'number' ? { waitMs: profileCommand.waitMs } : {}),
|
|
831
|
+
...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
|
|
536
832
|
}),
|
|
537
833
|
waitMs: profileCommand.waitMs,
|
|
538
834
|
})),
|
|
@@ -565,13 +861,18 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
565
861
|
...(typeof args.adb === 'string' ? { adbPath: args.adb } : {}),
|
|
566
862
|
captureLogcat: true,
|
|
567
863
|
clearLogcat: isEnabled(args['clear-logcat']),
|
|
864
|
+
commandTimeoutMs: adbCommandTimeoutMs,
|
|
568
865
|
deepLinks: profileSessionDeepLinks,
|
|
569
866
|
...(options.delay ? { delay: options.delay } : {}),
|
|
570
867
|
...(options.executor ? { executor: options.executor } : {}),
|
|
571
868
|
driverSteps,
|
|
572
869
|
launch: isEnabled(args.launch),
|
|
573
870
|
launchWaitMs: parsePositiveInteger(readScalarArg(args['launch-wait-ms']), 0),
|
|
574
|
-
logcatLines:
|
|
871
|
+
logcatLines: resolveProfileSessionLogcatLines({
|
|
872
|
+
args,
|
|
873
|
+
commands: profileSessionCommands,
|
|
874
|
+
profileSessionEnabled,
|
|
875
|
+
}),
|
|
575
876
|
outputDir: resolveAdbCaptureOutputDir({ args, runId }),
|
|
576
877
|
packageName: resolveAndroidPackageName({ args, config }),
|
|
577
878
|
...(typeof args['react-native-debug-host'] === 'string'
|
|
@@ -635,9 +936,46 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
635
936
|
const profileArgs = agentDeviceCapture
|
|
636
937
|
? appendAgentDeviceCaptureArgs({ args: baseProfileArgs, capture: agentDeviceCapture })
|
|
637
938
|
: baseProfileArgs;
|
|
939
|
+
const lifecyclePhase = resolveManifestLifecyclePhase(args);
|
|
940
|
+
const environmentSource = agentDeviceCapture ? 'agent-device' : 'adb';
|
|
941
|
+
const copiedAdbLogArtifact = adbCapture ? 'raw/adb-logcat.txt' : undefined;
|
|
638
942
|
return runProfileMobile(profileArgs, {
|
|
943
|
+
commandTransport: profileSessionStorageEnabled
|
|
944
|
+
? 'profile-session-storage'
|
|
945
|
+
: profileSessionEnabled
|
|
946
|
+
? 'profile-session-deeplink'
|
|
947
|
+
: agentDeviceCapture
|
|
948
|
+
? 'agent-device'
|
|
949
|
+
: 'adb-capture',
|
|
639
950
|
...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
|
|
640
951
|
defaultDriver: 'adb-logcat',
|
|
952
|
+
environmentPostconditions: {
|
|
953
|
+
appState: {
|
|
954
|
+
value: 'foreground',
|
|
955
|
+
evidence: 'asserted',
|
|
956
|
+
source: environmentSource,
|
|
957
|
+
...(copiedAdbLogArtifact ? { artifact: copiedAdbLogArtifact } : {}),
|
|
958
|
+
},
|
|
959
|
+
lifecyclePhase: {
|
|
960
|
+
value: 'foreground',
|
|
961
|
+
evidence: 'asserted',
|
|
962
|
+
source: environmentSource,
|
|
963
|
+
...(copiedAdbLogArtifact ? { artifact: copiedAdbLogArtifact } : {}),
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
environmentPreconditions: {
|
|
967
|
+
foregroundState: {
|
|
968
|
+
value: 'controlled-by-runner',
|
|
969
|
+
evidence: 'asserted',
|
|
970
|
+
source: environmentSource,
|
|
971
|
+
},
|
|
972
|
+
lifecyclePhase: {
|
|
973
|
+
value: lifecyclePhase,
|
|
974
|
+
evidence: 'asserted',
|
|
975
|
+
source: environmentSource,
|
|
976
|
+
...(copiedAdbLogArtifact ? { artifact: copiedAdbLogArtifact } : {}),
|
|
977
|
+
},
|
|
978
|
+
},
|
|
641
979
|
interactionDriver: agentDeviceCapture ? 'agent-device' : 'adb-logcat',
|
|
642
980
|
platform: 'android',
|
|
643
981
|
});
|
|
@@ -8,8 +8,13 @@ type IosProfileOptions = {
|
|
|
8
8
|
};
|
|
9
9
|
type IosSimctlProfileCommand = {
|
|
10
10
|
command: string;
|
|
11
|
+
commandId?: string;
|
|
11
12
|
label?: string;
|
|
13
|
+
queueId?: string;
|
|
14
|
+
sequence?: number;
|
|
15
|
+
waitForMilestone?: string;
|
|
12
16
|
waitMs?: number;
|
|
17
|
+
waitTimeoutMs?: number;
|
|
13
18
|
};
|
|
14
19
|
/**
|
|
15
20
|
* Resolves the simctl capture output directory for a profile run.
|
|
@@ -44,15 +49,21 @@ declare function resolveIosConflictingBundleIds(config: Record<string, any>): st
|
|
|
44
49
|
* @param {{config: Record<string, unknown>, action: 'start' | 'command', scenario: string, runId: string, command?: string}} options
|
|
45
50
|
* @returns {string}
|
|
46
51
|
*/
|
|
47
|
-
declare function buildProfileSessionUrl({ action, command, config, runId, scenario, }: {
|
|
52
|
+
declare function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitMs, waitTimeoutMs, }: {
|
|
48
53
|
action: 'start' | 'command';
|
|
49
54
|
command?: string;
|
|
55
|
+
commandId?: string;
|
|
50
56
|
config: Record<string, any>;
|
|
57
|
+
queueId?: string;
|
|
51
58
|
runId: string;
|
|
52
59
|
scenario: string;
|
|
60
|
+
sequence?: number;
|
|
61
|
+
waitForMilestone?: string;
|
|
62
|
+
waitMs?: number;
|
|
63
|
+
waitTimeoutMs?: number;
|
|
53
64
|
}): string;
|
|
54
65
|
/**
|
|
55
|
-
* 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.
|
|
56
67
|
*
|
|
57
68
|
* @param {Record<string, unknown>} scenario
|
|
58
69
|
* @returns {number}
|