agent-scenario-loop 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/app/profile-session.ts +812 -0
- package/core/config-template.json +41 -0
- package/dist/core/agent-summary.d.ts +15 -0
- package/dist/core/agent-summary.js +177 -0
- package/dist/core/artifact-contract.d.ts +151 -0
- package/dist/core/artifact-contract.js +897 -0
- package/dist/core/artifact-layout.d.ts +56 -0
- package/dist/core/artifact-layout.js +61 -0
- package/dist/core/artifact-writer.d.ts +44 -0
- package/dist/core/artifact-writer.js +55 -0
- package/dist/core/comparison.d.ts +133 -0
- package/dist/core/comparison.js +294 -0
- package/dist/core/evidence-interpreter.d.ts +28 -0
- package/dist/core/evidence-interpreter.js +69 -0
- package/dist/core/execution-plan.d.ts +44 -0
- package/dist/core/execution-plan.js +95 -0
- package/dist/core/planner.d.ts +132 -0
- package/dist/core/planner.js +812 -0
- package/dist/core/ports.d.ts +198 -0
- package/dist/core/ports.js +146 -0
- package/dist/core/run-index.d.ts +62 -0
- package/dist/core/run-index.js +143 -0
- package/dist/core/schema-validator.d.ts +86 -0
- package/dist/core/schema-validator.js +407 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +27 -0
- package/dist/runner/agent-device-driver.d.ts +126 -0
- package/dist/runner/agent-device-driver.js +168 -0
- package/dist/runner/agent-device.d.ts +295 -0
- package/dist/runner/agent-device.js +1271 -0
- package/dist/runner/android-adb-driver.d.ts +175 -0
- package/dist/runner/android-adb-driver.js +399 -0
- package/dist/runner/android-adb.d.ts +254 -0
- package/dist/runner/android-adb.js +1618 -0
- package/dist/runner/argent-driver.d.ts +183 -0
- package/dist/runner/argent-driver.js +297 -0
- package/dist/runner/argent.d.ts +349 -0
- package/dist/runner/argent.js +1211 -0
- package/dist/runner/check-plan.d.ts +45 -0
- package/dist/runner/check-plan.js +210 -0
- package/dist/runner/cli.d.ts +20 -0
- package/dist/runner/cli.js +23 -0
- package/dist/runner/compare-latest.d.ts +99 -0
- package/dist/runner/compare-latest.js +233 -0
- package/dist/runner/compare.d.ts +58 -0
- package/dist/runner/compare.js +157 -0
- package/dist/runner/demo-loop.d.ts +45 -0
- package/dist/runner/demo-loop.js +170 -0
- package/dist/runner/example-android-live.d.ts +137 -0
- package/dist/runner/example-android-live.js +454 -0
- package/dist/runner/example-ios-live.d.ts +137 -0
- package/dist/runner/example-ios-live.js +471 -0
- package/dist/runner/host-doctor.d.ts +131 -0
- package/dist/runner/host-doctor.js +628 -0
- package/dist/runner/init-project.d.ts +88 -0
- package/dist/runner/init-project.js +263 -0
- package/dist/runner/ios-simctl-driver.d.ts +69 -0
- package/dist/runner/ios-simctl-driver.js +97 -0
- package/dist/runner/ios-simctl.d.ts +254 -0
- package/dist/runner/ios-simctl.js +1415 -0
- package/dist/runner/live-android.d.ts +137 -0
- package/dist/runner/live-android.js +539 -0
- package/dist/runner/live-comparison.d.ts +67 -0
- package/dist/runner/live-comparison.js +147 -0
- package/dist/runner/live-ios.d.ts +137 -0
- package/dist/runner/live-ios.js +460 -0
- package/dist/runner/live-proof-summary.d.ts +263 -0
- package/dist/runner/live-proof-summary.js +465 -0
- package/dist/runner/live-proof.d.ts +467 -0
- package/dist/runner/live-proof.js +920 -0
- package/dist/runner/local-env.d.ts +64 -0
- package/dist/runner/local-env.js +155 -0
- package/dist/runner/profile-android.d.ts +82 -0
- package/dist/runner/profile-android.js +671 -0
- package/dist/runner/profile-ios.d.ts +108 -0
- package/dist/runner/profile-ios.js +532 -0
- package/dist/runner/profile-mobile.d.ts +254 -0
- package/dist/runner/profile-mobile.js +1307 -0
- package/dist/runner/validate-project.d.ts +273 -0
- package/dist/runner/validate-project.js +1501 -0
- package/docs/adapters.md +145 -0
- package/docs/api.md +94 -0
- package/docs/authoring.md +196 -0
- package/docs/concepts.md +136 -0
- package/docs/consumer-rehearsal.md +115 -0
- package/docs/contracts.md +267 -0
- package/docs/live-proofs.md +270 -0
- package/docs/principles.md +46 -0
- package/examples/event-logs/app-startup-baseline.log +4 -0
- package/examples/event-logs/app-startup-current.log +4 -0
- package/examples/minimal-app/README.md +70 -0
- package/examples/mobile-app/README.md +302 -0
- package/examples/mobile-app/app.json +22 -0
- package/examples/mobile-app/asl/package-scripts.json +32 -0
- package/examples/mobile-app/asl.config.json +37 -0
- package/examples/mobile-app/event-logs/android-app-startup.log +4 -0
- package/examples/mobile-app/event-logs/android-open-close-cycle.log +12 -0
- package/examples/mobile-app/event-logs/android-scroll-settle.log +12 -0
- package/examples/mobile-app/event-logs/app-startup.log +4 -0
- package/examples/mobile-app/event-logs/open-close-cycle.log +12 -0
- package/examples/mobile-app/event-logs/scroll-settle.log +12 -0
- package/examples/mobile-app/index.ts +20 -0
- package/examples/mobile-app/metro.config.js +20 -0
- package/examples/mobile-app/package.json +62 -0
- package/examples/mobile-app/patches/expo-modules-jsi@56.0.10.patch +19 -0
- package/examples/mobile-app/plugins/with-ios-build-compat.js +271 -0
- package/examples/mobile-app/pnpm-lock.yaml +4440 -0
- package/examples/mobile-app/runner-manifests/evidence-provider.json +79 -0
- package/examples/mobile-app/runner-manifests/primary-runner.json +19 -0
- package/examples/mobile-app/scenarios/android/app-startup-video.json +73 -0
- package/examples/mobile-app/scenarios/android/app-startup.json +44 -0
- package/examples/mobile-app/scenarios/android/open-close-cycle.json +54 -0
- package/examples/mobile-app/scenarios/android/scroll-settle.json +49 -0
- package/examples/mobile-app/scenarios/ios/app-startup.json +44 -0
- package/examples/mobile-app/scenarios/ios/open-close-cycle.json +54 -0
- package/examples/mobile-app/scenarios/ios/scroll-settle.json +49 -0
- package/examples/mobile-app/scenarios/mobile/app-startup.json +91 -0
- package/examples/mobile-app/scenarios/mobile/open-close-cycle.json +160 -0
- package/examples/mobile-app/scenarios/mobile/scroll-settle.json +148 -0
- package/examples/mobile-app/scripts/asl-capture-accessibility-provider.mjs +112 -0
- package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +127 -0
- package/examples/mobile-app/src/devtools/profile-session.ts +7 -0
- package/examples/mobile-app/src/example-screen.tsx +322 -0
- package/examples/mobile-app/tsconfig.json +16 -0
- package/examples/mobile-app/tsconfig.typecheck.json +13 -0
- package/examples/runners/README.md +44 -0
- package/examples/runners/adb-android.json +25 -0
- package/examples/runners/agent-device-android.json +27 -0
- package/examples/runners/agent-device-ios.json +27 -0
- package/examples/runners/argent-android.json +32 -0
- package/examples/runners/argent-ios.json +32 -0
- package/examples/runners/argent-react-profiler-provider.json +15 -0
- package/examples/runners/axe-accessibility-provider.json +24 -0
- package/examples/runners/manual-log-ingest.json +9 -0
- package/examples/runners/rozenite-profiler-provider.json +9 -0
- package/examples/runners/script-accessibility-provider.json +24 -0
- package/examples/runners/script-memory-provider.json +24 -0
- package/examples/runners/script-network-provider.json +24 -0
- package/examples/runners/script-profiler-provider.json +30 -0
- package/examples/runners/xcodebuildmcp-ios.json +29 -0
- package/examples/scenarios/ios/app-startup.json +28 -0
- package/examples/scenarios/ios/open-close-cycle.json +35 -0
- package/examples/scenarios/mobile/app-startup.json +72 -0
- package/examples/scenarios/mobile/media-open-close.json +141 -0
- package/examples/scenarios/mobile/open-close-cycle.json +135 -0
- package/examples/scenarios/mobile/scroll-settle.json +106 -0
- package/package.json +240 -0
- package/schemas/budget-verdict.schema.json +115 -0
- package/schemas/causal-run.schema.json +279 -0
- package/schemas/comparison.schema.json +196 -0
- package/schemas/health.schema.json +108 -0
- package/schemas/live-proof-set.schema.json +195 -0
- package/schemas/live-proof.schema.json +413 -0
- package/schemas/manifest.schema.json +204 -0
- package/schemas/metrics.schema.json +137 -0
- package/schemas/project-validation.schema.json +343 -0
- package/schemas/runner-capabilities.schema.json +217 -0
- package/schemas/scenario.schema.json +400 -0
- package/schemas/verdict.schema.json +88 -0
- package/templates/evidence-provider.json +83 -0
- package/templates/gitignore-snippet +9 -0
- package/templates/integration-readme.md +125 -0
- package/templates/mobile-scenario.json +133 -0
- package/templates/package-scripts.json +32 -0
- package/templates/primary-runner.json +19 -0
- package/templates/project.config.json +37 -0
- package/templates/scripts/asl-capture-accessibility-provider.mjs +112 -0
- package/templates/scripts/asl-capture-profiler-provider.mjs +127 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.assertAggregatePassed = assertAggregatePassed;
|
|
5
|
+
exports.buildLiveRunId = buildLiveRunId;
|
|
6
|
+
exports.formatResult = formatResult;
|
|
7
|
+
exports.assertNoRegressedComparisons = assertNoRegressedComparisons;
|
|
8
|
+
exports.buildSkippedInteractionProofs = buildSkippedInteractionProofs;
|
|
9
|
+
exports.buildInteractionComparisonLane = buildInteractionComparisonLane;
|
|
10
|
+
exports.isTrustedProfileRun = isTrustedProfileRun;
|
|
11
|
+
exports.main = main;
|
|
12
|
+
exports.normalizeRunSuffix = normalizeRunSuffix;
|
|
13
|
+
exports.resolveIosDeviceId = resolveIosDeviceId;
|
|
14
|
+
exports.runExampleIosLiveProof = runExampleIosLiveProof;
|
|
15
|
+
exports.usage = usage;
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const { hasHelpFlag, writeUsage } = require('./cli');
|
|
19
|
+
const { parsePositiveInteger, parseArgs, runIosSimctlCapture, } = require('./ios-simctl');
|
|
20
|
+
const { compareLiveProfilesToLatest, isEnabledFlag, } = require('./live-comparison');
|
|
21
|
+
const { writeLiveProofSummary } = require('./live-proof-summary');
|
|
22
|
+
const { runAgentDeviceCapture } = require('./agent-device');
|
|
23
|
+
const { parseBaseArgs: parseArgentBaseArgs, runArgentCapture } = require('./argent');
|
|
24
|
+
const { loadAslLocalEnv, readStringArgOrEnv } = require('./local-env');
|
|
25
|
+
const { runProfileIos } = require('./profile-ios');
|
|
26
|
+
const EXAMPLE_PROFILES = [
|
|
27
|
+
{
|
|
28
|
+
label: 'startup',
|
|
29
|
+
runId: 'ios-live-startup',
|
|
30
|
+
scenario: 'app-startup.json',
|
|
31
|
+
scenarioId: 'app-startup',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: 'open-close',
|
|
35
|
+
runId: 'ios-live-open-close',
|
|
36
|
+
scenario: 'open-close-cycle.json',
|
|
37
|
+
scenarioId: 'open-close-cycle',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: 'scroll',
|
|
41
|
+
runId: 'ios-live-scroll',
|
|
42
|
+
scenario: 'scroll-settle.json',
|
|
43
|
+
scenarioId: 'scroll-settle',
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Prints CLI usage.
|
|
48
|
+
*
|
|
49
|
+
* @param {{write: (message: string) => unknown}} [output]
|
|
50
|
+
* @returns {void}
|
|
51
|
+
*/
|
|
52
|
+
function usage(output = process.stderr) {
|
|
53
|
+
writeUsage([
|
|
54
|
+
'Usage: asl-example-ios-live [--config <path>] [--out <dir>] [--bundle <id>] [--device <udid|booted>] [--xcrun <path>] [--ios-dev-client-url <url>] [--run-suffix <label>] [--compare-latest] [--fail-on-regression] [--agent-device-proof] [--argent-proof]',
|
|
55
|
+
'',
|
|
56
|
+
'Runs the packaged example iOS live proof: simctl preflight, startup, open-close, and scroll-settle.',
|
|
57
|
+
'The example app must already be installed on a booted iOS simulator and connected to Metro. Set ASL_EXAMPLE_IOS_DEV_CLIENT_URL for Expo dev-client builds that need an explicit Metro URL.',
|
|
58
|
+
'Use --run-suffix to preserve multiple live proof artifact sets without changing deterministic default run ids.',
|
|
59
|
+
'Use --compare-latest to compare each passed scenario against the latest trusted prior run under the artifact root.',
|
|
60
|
+
'Use --fail-on-regression with --compare-latest to exit nonzero after writing evidence when any comparison regressed.',
|
|
61
|
+
'Use --agent-device-proof to attach the shared startup UI assertion through agent-device; pass --agent-device-session-mode bind when a named session should still receive the configured UDID.',
|
|
62
|
+
'Use --argent-proof to attach the shared startup UI assertion through Argent; set ASL_ARGENT_BIN and ASL_ARGENT_BASE_ARGS for non-global installs. iOS Argent screenshots fall back to simctl when Argent screenshot is unavailable.',
|
|
63
|
+
], output);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolves the package root for source and built CLI execution.
|
|
67
|
+
*
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
function defaultPackageRoot() {
|
|
71
|
+
return path.resolve(__dirname, '..', '..');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Reads a JSON object from disk.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} filePath
|
|
77
|
+
* @returns {Record<string, unknown>}
|
|
78
|
+
*/
|
|
79
|
+
function readJson(filePath) {
|
|
80
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Reads a nested object property from unknown JSON.
|
|
84
|
+
*
|
|
85
|
+
* @param {Record<string, unknown>} value
|
|
86
|
+
* @param {string} key
|
|
87
|
+
* @returns {Record<string, unknown> | null}
|
|
88
|
+
*/
|
|
89
|
+
function readObjectProperty(value, key) {
|
|
90
|
+
const property = value[key];
|
|
91
|
+
return property && typeof property === 'object' && !Array.isArray(property)
|
|
92
|
+
? property
|
|
93
|
+
: null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Resolves the iOS bundle id from explicit input or ASL config.
|
|
97
|
+
*
|
|
98
|
+
* @param {{args: CliArgs, config: Record<string, unknown>}} options
|
|
99
|
+
* @returns {string | null}
|
|
100
|
+
*/
|
|
101
|
+
function resolveIosBundleId({ args, config, }) {
|
|
102
|
+
if (typeof args.bundle === 'string') {
|
|
103
|
+
return args.bundle;
|
|
104
|
+
}
|
|
105
|
+
const app = readObjectProperty(config, 'app');
|
|
106
|
+
return typeof app?.iosBundleId === 'string' ? app.iosBundleId : null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolves optional sibling iOS bundle ids that make simulator targeting ambiguous.
|
|
110
|
+
*
|
|
111
|
+
* @param {Record<string, unknown>} config
|
|
112
|
+
* @returns {string[]}
|
|
113
|
+
*/
|
|
114
|
+
function resolveIosConflictingBundleIds(config) {
|
|
115
|
+
const app = readObjectProperty(config, 'app');
|
|
116
|
+
const configured = app?.iosConflictingBundleIds;
|
|
117
|
+
return Array.isArray(configured)
|
|
118
|
+
? configured.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
119
|
+
: [];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolves the simulator target used by every iOS proof lane.
|
|
123
|
+
*
|
|
124
|
+
* @param {CliArgs} args
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
function resolveIosDeviceId(args) {
|
|
128
|
+
return readStringArgOrEnv(args.device, ['ASL_IOS_UDID', 'ASL_EXAMPLE_IOS_UDID']) ?? 'booted';
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Converts a caller-provided run suffix into a path-safe run-id segment.
|
|
132
|
+
*
|
|
133
|
+
* @param {unknown} value
|
|
134
|
+
* @returns {string | null}
|
|
135
|
+
*/
|
|
136
|
+
function normalizeRunSuffix(value) {
|
|
137
|
+
if (typeof value !== 'string') {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const normalized = value
|
|
141
|
+
.trim()
|
|
142
|
+
.toLowerCase()
|
|
143
|
+
.replace(/[^a-z0-9._-]+/gu, '-')
|
|
144
|
+
.replace(/^-+|-+$/gu, '')
|
|
145
|
+
.slice(0, 64);
|
|
146
|
+
return normalized.length > 0 ? normalized : null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Applies the optional run suffix while preserving deterministic defaults.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} baseRunId
|
|
152
|
+
* @param {string | null} suffix
|
|
153
|
+
* @returns {string}
|
|
154
|
+
*/
|
|
155
|
+
function buildLiveRunId(baseRunId, suffix) {
|
|
156
|
+
return suffix ? `${baseRunId}-${suffix}` : baseRunId;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Builds the comparison lane suffix for enabled interaction proofs.
|
|
160
|
+
*
|
|
161
|
+
* @param {string[]} runnerIds
|
|
162
|
+
* @returns {string}
|
|
163
|
+
*/
|
|
164
|
+
function buildInteractionComparisonLane(runnerIds) {
|
|
165
|
+
return runnerIds.length > 0
|
|
166
|
+
? `example-ios-live+${runnerIds.join('+')}`
|
|
167
|
+
: 'example-ios-live';
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Reports whether profile evidence is healthy enough to trust sidecar proofs and comparisons.
|
|
171
|
+
*
|
|
172
|
+
* @param {{health: Record<string, unknown>, verdict: Record<string, unknown>}} run
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
function isTrustedProfileRun({ health, verdict, }) {
|
|
176
|
+
return health.healthStatus === 'passed' && verdict.verdictStatus === 'passed';
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Builds skipped sidecar pointers for requested example runners when the profile batch failed.
|
|
180
|
+
*
|
|
181
|
+
* @param {{failedProfiles: IosLiveProfile[], requestedRunners: string[], runIdsByRunner: Record<string, string>}} options
|
|
182
|
+
* @returns {IosSkippedInteractionProof[]}
|
|
183
|
+
*/
|
|
184
|
+
function buildSkippedInteractionProofs({ failedProfiles, requestedRunners, runIdsByRunner, }) {
|
|
185
|
+
if (failedProfiles.length === 0) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
const failedSummary = failedProfiles
|
|
189
|
+
.map((profile) => `${profile.label}=health:${profile.healthStatus ?? 'unknown'}/verdict:${profile.verdictStatus ?? 'unknown'}`)
|
|
190
|
+
.join(', ');
|
|
191
|
+
const reason = `Profile batch failed (${failedSummary}); sidecar interaction proof was skipped because aggregate runner evidence would not be trustworthy.`;
|
|
192
|
+
const labelsByRunner = {
|
|
193
|
+
'agent-device': 'startup-ui',
|
|
194
|
+
argent: 'startup-ui-argent',
|
|
195
|
+
};
|
|
196
|
+
return requestedRunners.map((runnerId) => ({
|
|
197
|
+
label: labelsByRunner[runnerId] ?? `startup-ui-${runnerId}`,
|
|
198
|
+
nextAction: {
|
|
199
|
+
code: 'fix_profile_gate',
|
|
200
|
+
summary: 'Inspect the failed profile health and verdict before rerunning sidecar interaction proofs.',
|
|
201
|
+
},
|
|
202
|
+
reason,
|
|
203
|
+
runId: runIdsByRunner[runnerId] ?? `ios-${runnerId}-startup`,
|
|
204
|
+
runnerId,
|
|
205
|
+
scenarioId: 'app-startup',
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Throws after aggregate proof writing when the live proof itself failed.
|
|
210
|
+
*
|
|
211
|
+
* @param {IosLiveProofResult} result
|
|
212
|
+
* @returns {void}
|
|
213
|
+
*/
|
|
214
|
+
function assertAggregatePassed(result) {
|
|
215
|
+
const proof = readJson(result.aggregateSummary.liveProofPath);
|
|
216
|
+
if (proof.status === 'passed') {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
throw new Error(`iOS example live proof failed. Inspect ${result.aggregateSummary.summaryPath}.`);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Throws after aggregate proof writing when fail-on-regression should gate the run.
|
|
223
|
+
*
|
|
224
|
+
* @param {RegressionGateOptions} options
|
|
225
|
+
* @returns {void}
|
|
226
|
+
*/
|
|
227
|
+
function assertNoRegressedComparisons({ platformLabel, result, }) {
|
|
228
|
+
const regressed = result.comparisons.filter((comparison) => comparison.status === 'worse');
|
|
229
|
+
if (regressed.length === 0) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const labels = regressed.map((comparison) => comparison.label).join(', ');
|
|
233
|
+
throw new Error(`${platformLabel} example live proof found regressed comparison(s): ${labels}. Inspect ${result.aggregateSummary.summaryPath}.`);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Runs simctl preflight plus all canonical iOS example live profiles.
|
|
237
|
+
*
|
|
238
|
+
* @param {CliArgs} args
|
|
239
|
+
* @param {IosLiveProofOptions} [options]
|
|
240
|
+
* @returns {Promise<IosLiveProofResult>}
|
|
241
|
+
*/
|
|
242
|
+
async function runExampleIosLiveProof(args, options = {}) {
|
|
243
|
+
const packageRoot = options.packageRoot ?? defaultPackageRoot();
|
|
244
|
+
const exampleRoot = path.join(packageRoot, 'examples', 'mobile-app');
|
|
245
|
+
const configPath = typeof args.config === 'string'
|
|
246
|
+
? path.resolve(args.config)
|
|
247
|
+
: path.join(exampleRoot, 'asl.config.json');
|
|
248
|
+
const outputDir = typeof args.out === 'string'
|
|
249
|
+
? path.resolve(args.out)
|
|
250
|
+
: path.resolve('artifacts/example-mobile-app/ios');
|
|
251
|
+
const config = readJson(configPath);
|
|
252
|
+
const bundleId = resolveIosBundleId({ args, config });
|
|
253
|
+
const deviceId = resolveIosDeviceId(args);
|
|
254
|
+
const agentDeviceSession = readStringArgOrEnv(args['agent-device-session'], [
|
|
255
|
+
'ASL_IOS_AGENT_DEVICE_SESSION',
|
|
256
|
+
'ASL_EXAMPLE_IOS_AGENT_DEVICE_SESSION',
|
|
257
|
+
]);
|
|
258
|
+
const agentDeviceSessionMode = readStringArgOrEnv(args['agent-device-session-mode'], [
|
|
259
|
+
'ASL_IOS_AGENT_DEVICE_SESSION_MODE',
|
|
260
|
+
'ASL_EXAMPLE_IOS_AGENT_DEVICE_SESSION_MODE',
|
|
261
|
+
]);
|
|
262
|
+
const runSuffix = normalizeRunSuffix(args['run-suffix']);
|
|
263
|
+
const iosDevClientUrl = readStringArgOrEnv(args['ios-dev-client-url'], [
|
|
264
|
+
'ASL_IOS_DEV_CLIENT_URL',
|
|
265
|
+
'ASL_EXAMPLE_IOS_DEV_CLIENT_URL',
|
|
266
|
+
]);
|
|
267
|
+
const iosDevClientWaitMs = readStringArgOrEnv(args['ios-dev-client-wait-ms'], [
|
|
268
|
+
'ASL_IOS_DEV_CLIENT_WAIT_MS',
|
|
269
|
+
'ASL_EXAMPLE_IOS_DEV_CLIENT_WAIT_MS',
|
|
270
|
+
]);
|
|
271
|
+
const aggregateRunId = buildLiveRunId('ios-live-proof', runSuffix);
|
|
272
|
+
const preflightRunId = buildLiveRunId('ios-live-preflight', runSuffix);
|
|
273
|
+
const agentDeviceRunId = buildLiveRunId('ios-agent-device-startup', runSuffix);
|
|
274
|
+
const argentRunId = buildLiveRunId('ios-argent-startup', runSuffix);
|
|
275
|
+
const enabledInteractionRunners = [
|
|
276
|
+
...(isEnabledFlag(args['agent-device-proof']) ? ['agent-device'] : []),
|
|
277
|
+
...(isEnabledFlag(args['argent-proof']) ? ['argent'] : []),
|
|
278
|
+
];
|
|
279
|
+
const comparisonLane = buildInteractionComparisonLane(enabledInteractionRunners);
|
|
280
|
+
const runIdsByRunner = {
|
|
281
|
+
'agent-device': agentDeviceRunId,
|
|
282
|
+
argent: argentRunId,
|
|
283
|
+
};
|
|
284
|
+
const preflightDir = path.join(outputDir, '_preflight', preflightRunId);
|
|
285
|
+
const preflight = await runIosSimctlCapture({
|
|
286
|
+
bundleId,
|
|
287
|
+
conflictingBundleIds: resolveIosConflictingBundleIds(config),
|
|
288
|
+
device: deviceId,
|
|
289
|
+
...(options.executor ? { executor: options.executor } : {}),
|
|
290
|
+
outputDir: preflightDir,
|
|
291
|
+
runId: preflightRunId,
|
|
292
|
+
...(typeof args.xcrun === 'string' ? { xcrunPath: args.xcrun } : {}),
|
|
293
|
+
});
|
|
294
|
+
if (preflight.health.healthStatus !== 'passed') {
|
|
295
|
+
throw new Error(`iOS live proof preflight failed; inspect ${preflight.runDir}/agent-summary.md.`);
|
|
296
|
+
}
|
|
297
|
+
const interactionProofs = [];
|
|
298
|
+
const profiles = [];
|
|
299
|
+
const failedProfiles = [];
|
|
300
|
+
for (const profile of EXAMPLE_PROFILES) {
|
|
301
|
+
const profileRunId = buildLiveRunId(profile.runId, runSuffix);
|
|
302
|
+
const result = await runProfileIos({
|
|
303
|
+
config: configPath,
|
|
304
|
+
device: deviceId,
|
|
305
|
+
...(typeof args['log-last'] === 'string' ? { 'log-last': args['log-last'] } : {}),
|
|
306
|
+
launch: true,
|
|
307
|
+
out: outputDir,
|
|
308
|
+
...(iosDevClientUrl ? { 'ios-dev-client-url': iosDevClientUrl } : {}),
|
|
309
|
+
...(iosDevClientWaitMs ? { 'ios-dev-client-wait-ms': iosDevClientWaitMs } : {}),
|
|
310
|
+
'profile-session': true,
|
|
311
|
+
'profile-session-storage': true,
|
|
312
|
+
'run-id': profileRunId,
|
|
313
|
+
scenario: path.join(exampleRoot, 'scenarios', 'mobile', profile.scenario),
|
|
314
|
+
'simctl-capture': true,
|
|
315
|
+
'simctl-out': path.join(outputDir, '_ios-simctl-captures', profileRunId),
|
|
316
|
+
...(typeof args['wait-ms'] === 'string' ? { 'wait-ms': args['wait-ms'] } : {}),
|
|
317
|
+
...(bundleId ? { bundle: bundleId } : {}),
|
|
318
|
+
...(typeof args.xcrun === 'string' ? { xcrun: args.xcrun } : {}),
|
|
319
|
+
}, {
|
|
320
|
+
comparisonLane,
|
|
321
|
+
...(options.delay ? { delay: options.delay } : {}),
|
|
322
|
+
...(options.executor ? { executor: options.executor } : {}),
|
|
323
|
+
});
|
|
324
|
+
const profilePointer = {
|
|
325
|
+
healthStatus: typeof result.health.healthStatus === 'string' ? result.health.healthStatus : 'unknown',
|
|
326
|
+
label: profile.label,
|
|
327
|
+
runDir: result.runDir,
|
|
328
|
+
runId: profileRunId,
|
|
329
|
+
scenario: profile.scenario,
|
|
330
|
+
scenarioId: profile.scenarioId,
|
|
331
|
+
verdictStatus: typeof result.verdict.verdictStatus === 'string' ? result.verdict.verdictStatus : 'unknown',
|
|
332
|
+
};
|
|
333
|
+
profiles.push(profilePointer);
|
|
334
|
+
if (!isTrustedProfileRun({ health: result.health, verdict: result.verdict })) {
|
|
335
|
+
failedProfiles.push(profilePointer);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const skippedInteractionProofs = buildSkippedInteractionProofs({
|
|
339
|
+
failedProfiles,
|
|
340
|
+
requestedRunners: enabledInteractionRunners,
|
|
341
|
+
runIdsByRunner,
|
|
342
|
+
});
|
|
343
|
+
const profileBatchTrusted = failedProfiles.length === 0;
|
|
344
|
+
if (profileBatchTrusted && isEnabledFlag(args['agent-device-proof'])) {
|
|
345
|
+
const agentDeviceCapture = await runAgentDeviceCapture({
|
|
346
|
+
...(typeof args['agent-device'] === 'string' ? { agentDevicePath: args['agent-device'] } : {}),
|
|
347
|
+
app: bundleId,
|
|
348
|
+
...(options.agentDeviceExecutor ? { executor: options.agentDeviceExecutor } : {}),
|
|
349
|
+
open: true,
|
|
350
|
+
outputDir: path.join(outputDir, '_agent-device-captures', agentDeviceRunId),
|
|
351
|
+
platform: 'ios',
|
|
352
|
+
runId: agentDeviceRunId,
|
|
353
|
+
scenario: readJson(path.join(exampleRoot, 'scenarios', 'mobile', 'app-startup.json')),
|
|
354
|
+
...(agentDeviceSession ? { session: agentDeviceSession } : {}),
|
|
355
|
+
...(agentDeviceSessionMode
|
|
356
|
+
? { sessionMode: agentDeviceSessionMode }
|
|
357
|
+
: {}),
|
|
358
|
+
udid: deviceId,
|
|
359
|
+
waitMs: parsePositiveInteger(args['agent-device-wait-ms'], 1000),
|
|
360
|
+
});
|
|
361
|
+
interactionProofs.push({
|
|
362
|
+
label: 'startup-ui',
|
|
363
|
+
runDir: agentDeviceCapture.runDir,
|
|
364
|
+
runId: agentDeviceRunId,
|
|
365
|
+
runnerId: 'agent-device',
|
|
366
|
+
scenarioId: 'app-startup',
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (profileBatchTrusted && isEnabledFlag(args['argent-proof'])) {
|
|
370
|
+
const argentBaseArgs = parseArgentBaseArgs(process.env.ASL_ARGENT_BASE_ARGS);
|
|
371
|
+
const argentCapture = await runArgentCapture({
|
|
372
|
+
app: bundleId,
|
|
373
|
+
argentCommand: process.env.ASL_ARGENT_BIN || 'argent',
|
|
374
|
+
...(argentBaseArgs ? { baseArgs: argentBaseArgs } : {}),
|
|
375
|
+
commandTimeoutMs: parsePositiveInteger(process.env.ASL_ARGENT_COMMAND_TIMEOUT_MS, 60_000),
|
|
376
|
+
deviceId,
|
|
377
|
+
...(options.delay ? { delay: options.delay } : {}),
|
|
378
|
+
...(options.argentExecutor ? { executor: options.argentExecutor } : {}),
|
|
379
|
+
iosSimctlScreenshotFallback: true,
|
|
380
|
+
...(options.executor ? { iosSimctlExecutor: options.executor } : {}),
|
|
381
|
+
outputDir: path.join(outputDir, '_argent-captures', argentRunId),
|
|
382
|
+
platform: 'ios',
|
|
383
|
+
runId: argentRunId,
|
|
384
|
+
scenario: readJson(path.join(exampleRoot, 'scenarios', 'mobile', 'app-startup.json')),
|
|
385
|
+
});
|
|
386
|
+
interactionProofs.push({
|
|
387
|
+
label: 'startup-ui-argent',
|
|
388
|
+
runDir: argentCapture.runDir,
|
|
389
|
+
runId: argentRunId,
|
|
390
|
+
runnerId: 'argent',
|
|
391
|
+
scenarioId: 'app-startup',
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const comparisons = profileBatchTrusted && isEnabledFlag(args['compare-latest'])
|
|
395
|
+
? await compareLiveProfilesToLatest({ outputDir, profiles })
|
|
396
|
+
: [];
|
|
397
|
+
const aggregateSummary = await writeLiveProofSummary({
|
|
398
|
+
comparisons,
|
|
399
|
+
interactionProofs,
|
|
400
|
+
outputDir,
|
|
401
|
+
platform: 'ios',
|
|
402
|
+
preflightDir: preflight.runDir,
|
|
403
|
+
preflightRunId,
|
|
404
|
+
profiles,
|
|
405
|
+
runId: aggregateRunId,
|
|
406
|
+
skippedInteractionProofs,
|
|
407
|
+
});
|
|
408
|
+
const result = {
|
|
409
|
+
aggregateSummary,
|
|
410
|
+
comparisons,
|
|
411
|
+
interactionProofs,
|
|
412
|
+
outputDir,
|
|
413
|
+
preflightDir: preflight.runDir,
|
|
414
|
+
profiles,
|
|
415
|
+
skippedInteractionProofs,
|
|
416
|
+
};
|
|
417
|
+
if (isEnabledFlag(args['fail-on-regression'])) {
|
|
418
|
+
assertNoRegressedComparisons({
|
|
419
|
+
platformLabel: 'iOS',
|
|
420
|
+
result,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
assertAggregatePassed(result);
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Formats the live proof result for humans and agents.
|
|
428
|
+
*
|
|
429
|
+
* @param {IosLiveProofResult} result
|
|
430
|
+
* @returns {string}
|
|
431
|
+
*/
|
|
432
|
+
function formatResult(result) {
|
|
433
|
+
return [
|
|
434
|
+
'iOS example live proof passed.',
|
|
435
|
+
`Live proof: ${result.aggregateSummary.summaryPath}`,
|
|
436
|
+
`Preflight: ${result.preflightDir}/agent-summary.md`,
|
|
437
|
+
...result.profiles.map((profile) => (`${profile.label}: ${profile.runDir}/agent-summary.md`)),
|
|
438
|
+
...result.interactionProofs.map((proof) => (`${proof.label}: ${proof.runDir}/agent-summary.md`)),
|
|
439
|
+
...(result.comparisons.length > 0
|
|
440
|
+
? [
|
|
441
|
+
'Comparisons:',
|
|
442
|
+
...result.comparisons.map((comparison) => (comparison.status === 'skipped'
|
|
443
|
+
? `${comparison.label}: skipped - ${comparison.reason}`
|
|
444
|
+
: `${comparison.label}: ${comparison.status} ${comparison.summaryPath}`)),
|
|
445
|
+
]
|
|
446
|
+
: []),
|
|
447
|
+
`Artifact root: ${result.outputDir}`,
|
|
448
|
+
].join('\n');
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Runs the example iOS live proof CLI.
|
|
452
|
+
*
|
|
453
|
+
* @returns {Promise<void>}
|
|
454
|
+
*/
|
|
455
|
+
async function main() {
|
|
456
|
+
const argv = process.argv.slice(2);
|
|
457
|
+
if (hasHelpFlag(argv)) {
|
|
458
|
+
usage(process.stdout);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
loadAslLocalEnv();
|
|
462
|
+
const args = parseArgs(argv);
|
|
463
|
+
const result = await runExampleIosLiveProof(args);
|
|
464
|
+
process.stdout.write(`${formatResult(result)}\n`);
|
|
465
|
+
}
|
|
466
|
+
if (require.main === module) {
|
|
467
|
+
main().catch((error) => {
|
|
468
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
469
|
+
process.exitCode = 1;
|
|
470
|
+
});
|
|
471
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
type HostDoctorRequirement = 'agent-device' | 'android' | 'argent' | 'ios';
|
|
3
|
+
type CliArgs = {
|
|
4
|
+
adb?: string | boolean;
|
|
5
|
+
'agent-device'?: string | boolean;
|
|
6
|
+
'agent-device-require-platforms'?: string | boolean;
|
|
7
|
+
'android-package'?: string | boolean;
|
|
8
|
+
'android-serial'?: string | boolean;
|
|
9
|
+
argent?: string | boolean;
|
|
10
|
+
'base-args'?: string | boolean;
|
|
11
|
+
'command-timeout-ms'?: string | boolean;
|
|
12
|
+
'ios-bundle'?: string | boolean;
|
|
13
|
+
'ios-device'?: string | boolean;
|
|
14
|
+
out?: string | boolean;
|
|
15
|
+
require?: string | boolean;
|
|
16
|
+
'run-id'?: string | boolean;
|
|
17
|
+
xcrun?: string | boolean;
|
|
18
|
+
[key: string]: string | boolean | undefined;
|
|
19
|
+
};
|
|
20
|
+
type HostDoctorChildResult = {
|
|
21
|
+
health?: Record<string, unknown>;
|
|
22
|
+
runDir: string;
|
|
23
|
+
verdict?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
type HostDoctorOptions = {
|
|
26
|
+
adbPath?: string;
|
|
27
|
+
agentDevicePath?: string;
|
|
28
|
+
agentDeviceRequiredPlatforms?: import('./agent-device-driver').AgentDevicePlatform[];
|
|
29
|
+
androidPackageName?: string | null;
|
|
30
|
+
androidPreflight?: (options: Record<string, unknown>) => Promise<HostDoctorChildResult>;
|
|
31
|
+
androidSerial?: string | null;
|
|
32
|
+
argentAvailability?: (options: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
33
|
+
argentCommand?: string;
|
|
34
|
+
argentBaseArgs?: string[];
|
|
35
|
+
commandTimeoutMs?: number;
|
|
36
|
+
iosBundleId?: string | null;
|
|
37
|
+
iosDevice?: string | null;
|
|
38
|
+
iosPreflight?: (options: Record<string, unknown>) => Promise<HostDoctorChildResult>;
|
|
39
|
+
outputDir?: string;
|
|
40
|
+
requirements?: HostDoctorRequirement[];
|
|
41
|
+
runId?: string;
|
|
42
|
+
agentDeviceAvailability?: (options: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
43
|
+
xcrunPath?: string;
|
|
44
|
+
};
|
|
45
|
+
type HostDoctorCheck = {
|
|
46
|
+
code: string;
|
|
47
|
+
message: string;
|
|
48
|
+
metadata?: Record<string, string | number | boolean | null>;
|
|
49
|
+
name: string;
|
|
50
|
+
source: 'runner';
|
|
51
|
+
status: 'failed' | 'passed';
|
|
52
|
+
};
|
|
53
|
+
type HostDoctorResult = {
|
|
54
|
+
agentSummary: string;
|
|
55
|
+
health: Record<string, unknown>;
|
|
56
|
+
raw: Record<string, unknown>;
|
|
57
|
+
runDir: string;
|
|
58
|
+
verdict: Record<string, unknown>;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Prints CLI usage.
|
|
62
|
+
*
|
|
63
|
+
* @param {{write: (message: string) => unknown}} output
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
declare function usage(output?: {
|
|
67
|
+
write: (message: string) => unknown;
|
|
68
|
+
}): void;
|
|
69
|
+
/**
|
|
70
|
+
* Parses `--key value` CLI arguments.
|
|
71
|
+
*
|
|
72
|
+
* @param {string[]} argv
|
|
73
|
+
* @returns {CliArgs}
|
|
74
|
+
*/
|
|
75
|
+
declare function parseArgs(argv: string[]): CliArgs;
|
|
76
|
+
/**
|
|
77
|
+
* Parses the comma-separated host doctor requirements.
|
|
78
|
+
*
|
|
79
|
+
* @param {unknown} value
|
|
80
|
+
* @returns {HostDoctorRequirement[]}
|
|
81
|
+
*/
|
|
82
|
+
declare function parseRequirements(value: unknown): HostDoctorRequirement[];
|
|
83
|
+
/**
|
|
84
|
+
* Builds a scalar ASL health check from a child preflight run.
|
|
85
|
+
*
|
|
86
|
+
* @param {{health: Record<string, unknown>, label: string, name: string, runDir: string}} options
|
|
87
|
+
* @returns {HostDoctorCheck}
|
|
88
|
+
*/
|
|
89
|
+
declare function buildChildRunCheck({ health, label, name, runDir, }: {
|
|
90
|
+
health: Record<string, unknown>;
|
|
91
|
+
label: string;
|
|
92
|
+
name: string;
|
|
93
|
+
runDir: string;
|
|
94
|
+
}): HostDoctorCheck;
|
|
95
|
+
/**
|
|
96
|
+
* Builds a scalar ASL health check from a command-surface availability result.
|
|
97
|
+
*
|
|
98
|
+
* @param {{label: string, name: string, rawPath: string, result: Record<string, unknown>}} options
|
|
99
|
+
* @returns {HostDoctorCheck}
|
|
100
|
+
*/
|
|
101
|
+
declare function buildAvailabilityCheck({ label, name, rawPath, result, }: {
|
|
102
|
+
label: string;
|
|
103
|
+
name: string;
|
|
104
|
+
rawPath: string;
|
|
105
|
+
result: Record<string, unknown>;
|
|
106
|
+
}): HostDoctorCheck;
|
|
107
|
+
/**
|
|
108
|
+
* Builds a host-specific agent summary for live-proof readiness.
|
|
109
|
+
*
|
|
110
|
+
* @param {{health: Record<string, unknown>, verdict: Record<string, unknown>}} options
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
declare function buildHostDoctorSummary({ health, verdict, }: {
|
|
114
|
+
health: Record<string, unknown>;
|
|
115
|
+
verdict: Record<string, unknown>;
|
|
116
|
+
}): string;
|
|
117
|
+
/**
|
|
118
|
+
* Runs host/device preflight checks and writes aggregate ASL artifacts.
|
|
119
|
+
*
|
|
120
|
+
* @param {HostDoctorOptions} options
|
|
121
|
+
* @returns {Promise<HostDoctorResult>}
|
|
122
|
+
*/
|
|
123
|
+
declare function runHostDoctor({ adbPath, agentDeviceAvailability, agentDevicePath, agentDeviceRequiredPlatforms, androidPackageName, androidPreflight, androidSerial, argentAvailability, argentBaseArgs, argentCommand, commandTimeoutMs, iosBundleId, iosDevice, iosPreflight, outputDir, requirements, runId, xcrunPath, }?: HostDoctorOptions): Promise<HostDoctorResult>;
|
|
124
|
+
/**
|
|
125
|
+
* Runs the host doctor CLI.
|
|
126
|
+
*
|
|
127
|
+
* @returns {Promise<void>}
|
|
128
|
+
*/
|
|
129
|
+
declare function main(): Promise<void>;
|
|
130
|
+
export { buildAvailabilityCheck, buildChildRunCheck, buildHostDoctorSummary, main, parseArgs, parseRequirements, runHostDoctor, usage, };
|
|
131
|
+
export type { CliArgs, HostDoctorCheck, HostDoctorOptions, HostDoctorRequirement, HostDoctorResult, };
|