agent-scenario-loop 0.1.2 → 0.1.3
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 +98 -4
- package/dist/core/agent-summary.d.ts +3 -2
- package/dist/core/agent-summary.js +44 -2
- package/dist/core/artifact-contract.d.ts +22 -4
- package/dist/core/artifact-contract.js +512 -11
- 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 +1 -0
- package/dist/core/schema-validator.js +1 -0
- 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 +5 -0
- package/dist/runner/ios-simctl.js +6 -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 +5 -0
- package/dist/runner/profile-android.js +148 -24
- package/dist/runner/profile-ios.d.ts +11 -1
- package/dist/runner/profile-ios.js +128 -9
- package/dist/runner/profile-mobile.d.ts +8 -0
- package/dist/runner/profile-mobile.js +267 -28
- package/docs/adapters.md +4 -0
- package/docs/architecture.md +90 -0
- package/docs/authoring.md +5 -1
- package/docs/concepts.md +3 -24
- package/docs/consumer-rehearsal.md +4 -0
- package/docs/contracts.md +30 -100
- package/docs/external-adapter-protocol.md +219 -0
- package/docs/live-proofs.md +83 -2
- package/docs/principles.md +9 -15
- package/examples/mobile-app/README.md +12 -0
- package/examples/mobile-app/runner-manifests/primary-runner.json +1 -0
- package/examples/runners/README.md +1 -0
- 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/xcodebuildmcp-ios.json +1 -0
- package/package.json +2 -1
- 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 +442 -1
- package/schemas/runner-capabilities.schema.json +20 -0
- package/schemas/scenario.schema.json +16 -0
- package/templates/primary-runner.json +1 -0
|
@@ -26,6 +26,21 @@ const PROFILE_SESSION_CAPTURE_BOOTSTRAP_MS = 1000;
|
|
|
26
26
|
const PROFILE_SESSION_CAPTURE_MAX_MS = 120000;
|
|
27
27
|
const DEFAULT_ANDROID_PROFILE_SESSION_STORAGE_KEY = 'agent-scenario-loop.profile-session.1';
|
|
28
28
|
const DEFAULT_ANDROID_PROFILE_COMMAND_STORAGE_KEY = 'agent-scenario-loop.profile-commands.1';
|
|
29
|
+
const MANIFEST_LIFECYCLE_PHASES = new Set([
|
|
30
|
+
'cold-launch',
|
|
31
|
+
'warm-launch',
|
|
32
|
+
'hot-launch',
|
|
33
|
+
'resume',
|
|
34
|
+
'foreground',
|
|
35
|
+
'background',
|
|
36
|
+
'force-stop',
|
|
37
|
+
'process-death',
|
|
38
|
+
'scene-recreation',
|
|
39
|
+
'activity-recreation',
|
|
40
|
+
'os-reclaim',
|
|
41
|
+
'reboot',
|
|
42
|
+
'relaunch',
|
|
43
|
+
]);
|
|
29
44
|
/**
|
|
30
45
|
* Reads and parses a JSON object from disk.
|
|
31
46
|
*
|
|
@@ -54,6 +69,22 @@ function isEnabled(value) {
|
|
|
54
69
|
function readPositiveInteger(value, fallback) {
|
|
55
70
|
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
56
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolves the lifecycle phase this runner is prepared to assert.
|
|
74
|
+
*
|
|
75
|
+
* @param {import('./profile-mobile').CliArgs} args
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
function resolveManifestLifecyclePhase(args) {
|
|
79
|
+
const lifecyclePhase = readScalarArg(args['lifecycle-phase']);
|
|
80
|
+
if (lifecyclePhase === undefined) {
|
|
81
|
+
return 'cold-launch';
|
|
82
|
+
}
|
|
83
|
+
if (typeof lifecyclePhase !== 'string' || !MANIFEST_LIFECYCLE_PHASES.has(lifecyclePhase)) {
|
|
84
|
+
throw new Error(`Unsupported --lifecycle-phase "${String(lifecyclePhase)}". Expected one of ${Array.from(MANIFEST_LIFECYCLE_PHASES).join(', ')}.`);
|
|
85
|
+
}
|
|
86
|
+
return lifecyclePhase;
|
|
87
|
+
}
|
|
57
88
|
/**
|
|
58
89
|
* Reads the number of scenario iterations that can emit app-owned truth events.
|
|
59
90
|
*
|
|
@@ -119,7 +150,7 @@ function resolveAndroidPackageName({ args, config, }) {
|
|
|
119
150
|
* @param {{config: Record<string, unknown>, action: 'start' | 'command', scenario: string, runId: string, command?: string}} options
|
|
120
151
|
* @returns {string}
|
|
121
152
|
*/
|
|
122
|
-
function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
|
|
153
|
+
function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitTimeoutMs, }) {
|
|
123
154
|
const scheme = typeof config.app?.profileSessionScheme === 'string'
|
|
124
155
|
? config.app.profileSessionScheme
|
|
125
156
|
: typeof config.app?.scheme === 'string'
|
|
@@ -128,6 +159,21 @@ function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
|
|
|
128
159
|
const params = new URLSearchParams({ runId, scenario });
|
|
129
160
|
if (action === 'command' && command) {
|
|
130
161
|
params.set('command', command);
|
|
162
|
+
if (commandId) {
|
|
163
|
+
params.set('commandId', commandId);
|
|
164
|
+
}
|
|
165
|
+
if (typeof sequence === 'number') {
|
|
166
|
+
params.set('sequence', String(sequence));
|
|
167
|
+
}
|
|
168
|
+
if (queueId) {
|
|
169
|
+
params.set('queueId', queueId);
|
|
170
|
+
}
|
|
171
|
+
if (waitForMilestone) {
|
|
172
|
+
params.set('waitForMilestone', waitForMilestone);
|
|
173
|
+
}
|
|
174
|
+
if (typeof waitTimeoutMs === 'number') {
|
|
175
|
+
params.set('waitTimeoutMs', String(waitTimeoutMs));
|
|
176
|
+
}
|
|
131
177
|
}
|
|
132
178
|
return `${scheme}://profile-session/${action}?${params.toString()}`;
|
|
133
179
|
}
|
|
@@ -138,6 +184,23 @@ function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
|
|
|
138
184
|
* @returns {import('./android-adb').AndroidAsyncStorageWrite[]}
|
|
139
185
|
*/
|
|
140
186
|
function buildProfileSessionStorageWrites({ commands, commandStorageKey, commandWaitMs, runId, scenario, sessionStorageKey, }) {
|
|
187
|
+
const timestampBase = Date.now();
|
|
188
|
+
const storedCommands = commands.map((profileCommand, index) => {
|
|
189
|
+
const timestamp = timestampBase + index + 1;
|
|
190
|
+
return {
|
|
191
|
+
id: `${timestamp}-${scenario}-${profileCommand.command}`,
|
|
192
|
+
scenario,
|
|
193
|
+
runId,
|
|
194
|
+
command: profileCommand.command,
|
|
195
|
+
...(typeof profileCommand.commandId === 'string' ? { commandId: profileCommand.commandId } : {}),
|
|
196
|
+
...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
|
|
197
|
+
...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
|
|
198
|
+
...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
|
|
199
|
+
...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
|
|
200
|
+
timestamp,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
const commandWaitMsTotal = commands.reduce((total, profileCommand) => (total + (typeof profileCommand.waitMs === 'number' && profileCommand.waitMs > 0 ? profileCommand.waitMs : 0)), 0);
|
|
141
204
|
return [
|
|
142
205
|
{
|
|
143
206
|
clearKeys: [commandStorageKey],
|
|
@@ -151,21 +214,14 @@ function buildProfileSessionStorageWrites({ commands, commandStorageKey, command
|
|
|
151
214
|
}).replace(`"${ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER}"`, ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER),
|
|
152
215
|
waitMs: commandWaitMs,
|
|
153
216
|
},
|
|
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
|
-
}),
|
|
217
|
+
...(storedCommands.length > 0
|
|
218
|
+
? [{
|
|
219
|
+
key: commandStorageKey,
|
|
220
|
+
label: 'profile-command-queue',
|
|
221
|
+
value: JSON.stringify(storedCommands),
|
|
222
|
+
...(commandWaitMsTotal > 0 ? { waitMs: commandWaitMsTotal } : {}),
|
|
223
|
+
}]
|
|
224
|
+
: []),
|
|
169
225
|
];
|
|
170
226
|
}
|
|
171
227
|
/**
|
|
@@ -271,14 +327,30 @@ function appendCaptureArg({ args, value, }) {
|
|
|
271
327
|
function resolveExecutionPlanProfileCommands(scenario) {
|
|
272
328
|
const executionPlan = buildScenarioExecutionPlan(scenario);
|
|
273
329
|
const repeat = readPositiveInteger(scenario.defaultIterations, readPositiveInteger(scenario.cycles?.iterations, 1));
|
|
274
|
-
const commands =
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
330
|
+
const commands = [];
|
|
331
|
+
for (const [index, step] of executionPlan.steps.entries()) {
|
|
332
|
+
if (step.portMethod !== 'executeStep' || typeof step.command !== 'string') {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const nextStep = executionPlan.steps[index + 1];
|
|
336
|
+
commands.push({
|
|
337
|
+
command: step.command,
|
|
338
|
+
commandId: step.id,
|
|
339
|
+
label: step.id,
|
|
340
|
+
queueId: scenario.id ?? scenario.name,
|
|
341
|
+
waitMs: readStepWaitMs(step),
|
|
342
|
+
...(nextStep?.portMethod === 'waitForTruthEvent' && typeof nextStep.milestone === 'string'
|
|
343
|
+
? {
|
|
344
|
+
waitForMilestone: nextStep.milestone,
|
|
345
|
+
waitTimeoutMs: readPositiveInteger(nextStep.timeoutMs, 0),
|
|
346
|
+
}
|
|
347
|
+
: {}),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return Array.from({ length: repeat }).flatMap((_, iteration) => commands.map((command, commandIndex) => ({
|
|
351
|
+
...command,
|
|
352
|
+
sequence: (iteration * commands.length) + commandIndex + 1,
|
|
353
|
+
})));
|
|
282
354
|
}
|
|
283
355
|
/**
|
|
284
356
|
* Expands normalized scenario evidence steps into Android adb driver actions.
|
|
@@ -392,7 +464,16 @@ function resolveAndroidAdbProfileCommands(scenario) {
|
|
|
392
464
|
}
|
|
393
465
|
commands.push({
|
|
394
466
|
command: command.command,
|
|
467
|
+
commandId: typeof command.id === 'string'
|
|
468
|
+
? command.id
|
|
469
|
+
: typeof command.commandId === 'string'
|
|
470
|
+
? command.commandId
|
|
471
|
+
: typeof command.label === 'string'
|
|
472
|
+
? command.label
|
|
473
|
+
: command.command,
|
|
395
474
|
...(typeof command.label === 'string' ? { label: command.label } : {}),
|
|
475
|
+
queueId: scenario.id ?? scenario.name,
|
|
476
|
+
sequence: commands.length + 1,
|
|
396
477
|
waitMs: readPositiveInteger(command.waitMs, 0),
|
|
397
478
|
});
|
|
398
479
|
}
|
|
@@ -459,6 +540,7 @@ function appendAgentDeviceCaptureArgs({ args, capture, }) {
|
|
|
459
540
|
async function runProfileAndroid(args, options = {}) {
|
|
460
541
|
if (!isEnabled(args['adb-capture']) && !isEnabled(args['agent-device-capture'])) {
|
|
461
542
|
return runProfileMobile(args, {
|
|
543
|
+
commandTransport: typeof args.events === 'string' ? 'fixture-log-ingest' : 'adb-artifacts',
|
|
462
544
|
...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
|
|
463
545
|
defaultDriver: 'adb-logcat',
|
|
464
546
|
...(typeof args['adb-artifacts'] === 'string' ? { interactionDriver: 'adb-logcat' } : {}),
|
|
@@ -530,9 +612,14 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
530
612
|
url: buildProfileSessionUrl({
|
|
531
613
|
action: 'command',
|
|
532
614
|
command: profileCommand.command,
|
|
615
|
+
...(typeof profileCommand.commandId === 'string' ? { commandId: profileCommand.commandId } : {}),
|
|
533
616
|
config,
|
|
534
617
|
runId,
|
|
535
618
|
scenario: scenarioName,
|
|
619
|
+
...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
|
|
620
|
+
...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
|
|
621
|
+
...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
|
|
622
|
+
...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
|
|
536
623
|
}),
|
|
537
624
|
waitMs: profileCommand.waitMs,
|
|
538
625
|
})),
|
|
@@ -635,9 +722,46 @@ async function runProfileAndroid(args, options = {}) {
|
|
|
635
722
|
const profileArgs = agentDeviceCapture
|
|
636
723
|
? appendAgentDeviceCaptureArgs({ args: baseProfileArgs, capture: agentDeviceCapture })
|
|
637
724
|
: baseProfileArgs;
|
|
725
|
+
const lifecyclePhase = resolveManifestLifecyclePhase(args);
|
|
726
|
+
const environmentSource = agentDeviceCapture ? 'agent-device' : 'adb';
|
|
727
|
+
const lifecycleArtifact = adbCapture ? 'raw/adb-logcat.txt' : 'raw/interaction.log';
|
|
638
728
|
return runProfileMobile(profileArgs, {
|
|
729
|
+
commandTransport: profileSessionStorageEnabled
|
|
730
|
+
? 'profile-session-storage'
|
|
731
|
+
: profileSessionEnabled
|
|
732
|
+
? 'profile-session-deeplink'
|
|
733
|
+
: agentDeviceCapture
|
|
734
|
+
? 'agent-device'
|
|
735
|
+
: 'adb-capture',
|
|
639
736
|
...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
|
|
640
737
|
defaultDriver: 'adb-logcat',
|
|
738
|
+
environmentPostconditions: {
|
|
739
|
+
appState: {
|
|
740
|
+
value: 'foreground',
|
|
741
|
+
evidence: 'asserted',
|
|
742
|
+
source: environmentSource,
|
|
743
|
+
artifact: lifecycleArtifact,
|
|
744
|
+
},
|
|
745
|
+
lifecyclePhase: {
|
|
746
|
+
value: 'foreground',
|
|
747
|
+
evidence: 'asserted',
|
|
748
|
+
source: environmentSource,
|
|
749
|
+
artifact: lifecycleArtifact,
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
environmentPreconditions: {
|
|
753
|
+
foregroundState: {
|
|
754
|
+
value: 'controlled-by-runner',
|
|
755
|
+
evidence: 'asserted',
|
|
756
|
+
source: environmentSource,
|
|
757
|
+
},
|
|
758
|
+
lifecyclePhase: {
|
|
759
|
+
value: lifecyclePhase,
|
|
760
|
+
evidence: 'asserted',
|
|
761
|
+
source: environmentSource,
|
|
762
|
+
artifact: lifecycleArtifact,
|
|
763
|
+
},
|
|
764
|
+
},
|
|
641
765
|
interactionDriver: agentDeviceCapture ? 'agent-device' : 'adb-logcat',
|
|
642
766
|
platform: 'android',
|
|
643
767
|
});
|
|
@@ -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,12 +49,17 @@ 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, 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
|
+
waitTimeoutMs?: number;
|
|
53
63
|
}): string;
|
|
54
64
|
/**
|
|
55
65
|
* Derives a storage-backed profile capture window from scenario waits and cycles.
|
|
@@ -34,6 +34,21 @@ const DEFAULT_IOS_PROFILE_COMMAND_STORAGE_KEY = 'agent-scenario-loop.profile-com
|
|
|
34
34
|
const DEFAULT_IOS_PROFILE_EVENT_STORAGE_KEY = 'agent-scenario-loop.profile-events.1';
|
|
35
35
|
const DEFAULT_IOS_PROFILE_SIGNAL_STORAGE_KEY = 'agent-scenario-loop.profile-signals.1';
|
|
36
36
|
const DEFAULT_IOS_PROFILE_SESSION_ENTRIES_STORAGE_KEY = 'agent-scenario-loop.profile-session-entries.1';
|
|
37
|
+
const MANIFEST_LIFECYCLE_PHASES = new Set([
|
|
38
|
+
'cold-launch',
|
|
39
|
+
'warm-launch',
|
|
40
|
+
'hot-launch',
|
|
41
|
+
'resume',
|
|
42
|
+
'foreground',
|
|
43
|
+
'background',
|
|
44
|
+
'force-stop',
|
|
45
|
+
'process-death',
|
|
46
|
+
'scene-recreation',
|
|
47
|
+
'activity-recreation',
|
|
48
|
+
'os-reclaim',
|
|
49
|
+
'reboot',
|
|
50
|
+
'relaunch',
|
|
51
|
+
]);
|
|
37
52
|
/**
|
|
38
53
|
* Reads and parses a JSON object from disk.
|
|
39
54
|
*
|
|
@@ -63,6 +78,22 @@ function readPositiveInteger(value, fallback) {
|
|
|
63
78
|
const parsed = typeof value === 'string' ? Number(value) : value;
|
|
64
79
|
return typeof parsed === 'number' && Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
65
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolves the lifecycle phase this runner is prepared to assert.
|
|
83
|
+
*
|
|
84
|
+
* @param {import('./profile-mobile').CliArgs} args
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
function resolveManifestLifecyclePhase(args) {
|
|
88
|
+
const lifecyclePhase = readScalarArg(args['lifecycle-phase']);
|
|
89
|
+
if (lifecyclePhase === undefined) {
|
|
90
|
+
return 'cold-launch';
|
|
91
|
+
}
|
|
92
|
+
if (typeof lifecyclePhase !== 'string' || !MANIFEST_LIFECYCLE_PHASES.has(lifecyclePhase)) {
|
|
93
|
+
throw new Error(`Unsupported --lifecycle-phase "${String(lifecyclePhase)}". Expected one of ${Array.from(MANIFEST_LIFECYCLE_PHASES).join(', ')}.`);
|
|
94
|
+
}
|
|
95
|
+
return lifecyclePhase;
|
|
96
|
+
}
|
|
66
97
|
/**
|
|
67
98
|
* Reads the number of scenario iterations that can emit app-owned truth events.
|
|
68
99
|
*
|
|
@@ -140,7 +171,7 @@ function resolveIosConflictingBundleIds(config) {
|
|
|
140
171
|
* @param {{config: Record<string, unknown>, action: 'start' | 'command', scenario: string, runId: string, command?: string}} options
|
|
141
172
|
* @returns {string}
|
|
142
173
|
*/
|
|
143
|
-
function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
|
|
174
|
+
function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitTimeoutMs, }) {
|
|
144
175
|
const scheme = typeof config.app?.profileSessionScheme === 'string'
|
|
145
176
|
? config.app.profileSessionScheme
|
|
146
177
|
: typeof config.app?.scheme === 'string'
|
|
@@ -149,6 +180,21 @@ function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
|
|
|
149
180
|
const params = new URLSearchParams({ runId, scenario });
|
|
150
181
|
if (action === 'command' && command) {
|
|
151
182
|
params.set('command', command);
|
|
183
|
+
if (commandId) {
|
|
184
|
+
params.set('commandId', commandId);
|
|
185
|
+
}
|
|
186
|
+
if (typeof sequence === 'number') {
|
|
187
|
+
params.set('sequence', String(sequence));
|
|
188
|
+
}
|
|
189
|
+
if (queueId) {
|
|
190
|
+
params.set('queueId', queueId);
|
|
191
|
+
}
|
|
192
|
+
if (waitForMilestone) {
|
|
193
|
+
params.set('waitForMilestone', waitForMilestone);
|
|
194
|
+
}
|
|
195
|
+
if (typeof waitTimeoutMs === 'number') {
|
|
196
|
+
params.set('waitTimeoutMs', String(waitTimeoutMs));
|
|
197
|
+
}
|
|
152
198
|
}
|
|
153
199
|
return `${scheme}://profile-session/${action}?${params.toString()}`;
|
|
154
200
|
}
|
|
@@ -211,14 +257,30 @@ function resolveProfileSessionCaptureWaitMs({ args, profileSessionEnabled, scena
|
|
|
211
257
|
function resolveExecutionPlanProfileCommands(scenario) {
|
|
212
258
|
const executionPlan = buildScenarioExecutionPlan(scenario);
|
|
213
259
|
const repeat = readPositiveInteger(scenario.defaultIterations, readPositiveInteger(scenario.cycles?.iterations, 1));
|
|
214
|
-
const commands =
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
260
|
+
const commands = [];
|
|
261
|
+
for (const [index, step] of executionPlan.steps.entries()) {
|
|
262
|
+
if (step.portMethod !== 'executeStep' || typeof step.command !== 'string') {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const nextStep = executionPlan.steps[index + 1];
|
|
266
|
+
commands.push({
|
|
267
|
+
command: step.command,
|
|
268
|
+
commandId: step.id,
|
|
269
|
+
label: step.id,
|
|
270
|
+
queueId: scenario.id ?? scenario.name,
|
|
271
|
+
waitMs: readStepWaitMs(step),
|
|
272
|
+
...(nextStep?.portMethod === 'waitForTruthEvent' && typeof nextStep.milestone === 'string'
|
|
273
|
+
? {
|
|
274
|
+
waitForMilestone: nextStep.milestone,
|
|
275
|
+
waitTimeoutMs: readPositiveInteger(nextStep.timeoutMs, 0),
|
|
276
|
+
}
|
|
277
|
+
: {}),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return Array.from({ length: repeat }).flatMap((_, iteration) => commands.map((command, commandIndex) => ({
|
|
281
|
+
...command,
|
|
282
|
+
sequence: (iteration * commands.length) + commandIndex + 1,
|
|
283
|
+
})));
|
|
222
284
|
}
|
|
223
285
|
/**
|
|
224
286
|
* Expands scenario-declared iOS commands for a simctl capture profile session.
|
|
@@ -240,7 +302,16 @@ function resolveIosSimctlProfileCommands(scenario) {
|
|
|
240
302
|
}
|
|
241
303
|
commands.push({
|
|
242
304
|
command: command.command,
|
|
305
|
+
commandId: typeof command.id === 'string'
|
|
306
|
+
? command.id
|
|
307
|
+
: typeof command.commandId === 'string'
|
|
308
|
+
? command.commandId
|
|
309
|
+
: typeof command.label === 'string'
|
|
310
|
+
? command.label
|
|
311
|
+
: command.command,
|
|
243
312
|
...(typeof command.label === 'string' ? { label: command.label } : {}),
|
|
313
|
+
queueId: scenario.id ?? scenario.name,
|
|
314
|
+
sequence: commands.length + 1,
|
|
244
315
|
waitMs: readPositiveInteger(command.waitMs, 0),
|
|
245
316
|
});
|
|
246
317
|
}
|
|
@@ -327,6 +398,7 @@ function appendAgentDeviceCaptureArgs({ args, capture, }) {
|
|
|
327
398
|
async function runProfileIos(args, options = {}) {
|
|
328
399
|
if (!isEnabled(args['simctl-capture']) && !isEnabled(args['agent-device-capture'])) {
|
|
329
400
|
return runProfileMobile(args, {
|
|
401
|
+
commandTransport: typeof args.events === 'string' ? 'fixture-log-ingest' : 'simctl-artifacts',
|
|
330
402
|
...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
|
|
331
403
|
defaultDriver: 'ios-simctl',
|
|
332
404
|
...(typeof args['simctl-artifacts'] === 'string' ? { interactionDriver: 'ios-simctl' } : {}),
|
|
@@ -397,9 +469,14 @@ async function runProfileIos(args, options = {}) {
|
|
|
397
469
|
url: buildProfileSessionUrl({
|
|
398
470
|
action: 'command',
|
|
399
471
|
command: profileCommand.command,
|
|
472
|
+
...(typeof profileCommand.commandId === 'string' ? { commandId: profileCommand.commandId } : {}),
|
|
400
473
|
config,
|
|
401
474
|
runId,
|
|
402
475
|
scenario: scenarioName,
|
|
476
|
+
...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
|
|
477
|
+
...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
|
|
478
|
+
...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
|
|
479
|
+
...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
|
|
403
480
|
}),
|
|
404
481
|
waitMs: profileCommand.waitMs,
|
|
405
482
|
})),
|
|
@@ -431,8 +508,13 @@ async function runProfileIos(args, options = {}) {
|
|
|
431
508
|
profileSessionStorage: {
|
|
432
509
|
commands: profileSessionCommands.map((profileCommand, index) => ({
|
|
433
510
|
command: profileCommand.command,
|
|
511
|
+
...(typeof profileCommand.commandId === 'string' ? { commandId: profileCommand.commandId } : {}),
|
|
434
512
|
id: `ios-storage-command-${index + 1}`,
|
|
435
513
|
...(typeof profileCommand.label === 'string' ? { label: profileCommand.label } : {}),
|
|
514
|
+
...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
|
|
515
|
+
...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
|
|
516
|
+
...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
|
|
517
|
+
...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
|
|
436
518
|
})),
|
|
437
519
|
runId,
|
|
438
520
|
scenario: scenarioName,
|
|
@@ -496,9 +578,46 @@ async function runProfileIos(args, options = {}) {
|
|
|
496
578
|
const profileArgs = agentDeviceCapture
|
|
497
579
|
? appendAgentDeviceCaptureArgs({ args: baseProfileArgs, capture: agentDeviceCapture })
|
|
498
580
|
: baseProfileArgs;
|
|
581
|
+
const lifecyclePhase = resolveManifestLifecyclePhase(args);
|
|
582
|
+
const environmentSource = agentDeviceCapture ? 'agent-device' : 'simctl';
|
|
583
|
+
const lifecycleArtifact = simctlCapture ? 'raw/ios-simctl-log.txt' : 'raw/interaction.log';
|
|
499
584
|
return runProfileMobile(profileArgs, {
|
|
585
|
+
commandTransport: agentDeviceCapture
|
|
586
|
+
? 'agent-device'
|
|
587
|
+
: profileSessionEnabled && !profileSessionStorageEnabled
|
|
588
|
+
? 'profile-session-deeplink'
|
|
589
|
+
: profileSessionEnabled
|
|
590
|
+
? 'profile-session-storage'
|
|
591
|
+
: 'simctl-capture',
|
|
500
592
|
...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
|
|
501
593
|
defaultDriver: 'ios-simctl',
|
|
594
|
+
environmentPostconditions: {
|
|
595
|
+
appState: {
|
|
596
|
+
value: 'foreground',
|
|
597
|
+
evidence: 'asserted',
|
|
598
|
+
source: environmentSource,
|
|
599
|
+
artifact: lifecycleArtifact,
|
|
600
|
+
},
|
|
601
|
+
lifecyclePhase: {
|
|
602
|
+
value: 'foreground',
|
|
603
|
+
evidence: 'asserted',
|
|
604
|
+
source: environmentSource,
|
|
605
|
+
artifact: lifecycleArtifact,
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
environmentPreconditions: {
|
|
609
|
+
foregroundState: {
|
|
610
|
+
value: 'controlled-by-runner',
|
|
611
|
+
evidence: 'asserted',
|
|
612
|
+
source: environmentSource,
|
|
613
|
+
},
|
|
614
|
+
lifecyclePhase: {
|
|
615
|
+
value: lifecyclePhase,
|
|
616
|
+
evidence: 'asserted',
|
|
617
|
+
source: environmentSource,
|
|
618
|
+
artifact: lifecycleArtifact,
|
|
619
|
+
},
|
|
620
|
+
},
|
|
502
621
|
interactionDriver: agentDeviceCapture ? 'agent-device' : 'ios-simctl',
|
|
503
622
|
platform: 'ios',
|
|
504
623
|
});
|
|
@@ -21,10 +21,14 @@ type ProfileRunResult = {
|
|
|
21
21
|
};
|
|
22
22
|
type ProfilePlatform = 'android' | 'ios';
|
|
23
23
|
type ProfileMobileOptions = {
|
|
24
|
+
commandTransport?: string;
|
|
24
25
|
comparisonLane?: string;
|
|
25
26
|
defaultDriver: string;
|
|
27
|
+
environmentPostconditions?: Record<string, unknown>;
|
|
28
|
+
environmentPreconditions?: Record<string, unknown>;
|
|
26
29
|
interactionDriver?: string;
|
|
27
30
|
platform: ProfilePlatform;
|
|
31
|
+
provenanceCohort?: Record<string, unknown>;
|
|
28
32
|
};
|
|
29
33
|
type CaptureEvidenceKind = 'screenshot' | 'uiTree' | 'video';
|
|
30
34
|
type ProviderEvidenceKind = 'accessibility' | 'logs' | 'profiler';
|
|
@@ -33,13 +37,17 @@ type EvidenceChannel = 'capture' | 'provider' | 'signal';
|
|
|
33
37
|
type EvidenceKind = CaptureEvidenceKind | ProviderEvidenceKind | SignalEvidenceKind;
|
|
34
38
|
type EvidenceAttachment = {
|
|
35
39
|
channel: EvidenceChannel;
|
|
40
|
+
completenessStatus: 'complete';
|
|
41
|
+
corruptionStatus: 'valid';
|
|
36
42
|
destinationPath: string;
|
|
37
43
|
kind: EvidenceKind;
|
|
38
44
|
manifestPath: string;
|
|
45
|
+
redactionStatus: 'not-redacted';
|
|
39
46
|
sha256: string;
|
|
40
47
|
sourcePath: string;
|
|
41
48
|
sourceFileName: string;
|
|
42
49
|
sizeBytes: number;
|
|
50
|
+
transformations: readonly ['copied'];
|
|
43
51
|
};
|
|
44
52
|
type EvidenceAttachmentInput = {
|
|
45
53
|
channel: EvidenceChannel;
|