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,1211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.argentDriverActionCode = argentDriverActionCode;
|
|
5
|
+
exports.buildArgentHealth = buildArgentHealth;
|
|
6
|
+
exports.buildArgentAvailabilityCheck = buildArgentAvailabilityCheck;
|
|
7
|
+
exports.buildArgentSelectorHealthMetadata = buildArgentSelectorHealthMetadata;
|
|
8
|
+
exports.buildArgentVerdict = buildArgentVerdict;
|
|
9
|
+
exports.checkArgentAvailability = checkArgentAvailability;
|
|
10
|
+
exports.copyArgentCapture = copyArgentCapture;
|
|
11
|
+
exports.defaultArgentRawFileName = defaultArgentRawFileName;
|
|
12
|
+
exports.defaultIosSimctlFallbackScreenshotFileName = defaultIosSimctlFallbackScreenshotFileName;
|
|
13
|
+
exports.deriveArgentRootArgs = deriveArgentRootArgs;
|
|
14
|
+
exports.execFileCommand = execFileCommand;
|
|
15
|
+
exports.execFileCommandWithTimeout = execFileCommandWithTimeout;
|
|
16
|
+
exports.isArgentSelector = isArgentSelector;
|
|
17
|
+
exports.main = main;
|
|
18
|
+
exports.parseArgs = parseArgs;
|
|
19
|
+
exports.parseBaseArgs = parseBaseArgs;
|
|
20
|
+
exports.readArgentStepOptions = readArgentStepOptions;
|
|
21
|
+
exports.readScreenSize = readScreenSize;
|
|
22
|
+
exports.resolveArgentDriverSteps = resolveArgentDriverSteps;
|
|
23
|
+
exports.runArgentCapture = runArgentCapture;
|
|
24
|
+
exports.runArgentDriverStep = runArgentDriverStep;
|
|
25
|
+
exports.runIosSimctlScreenshotFallback = runIosSimctlScreenshotFallback;
|
|
26
|
+
exports.sanitizeArtifactFileSegment = sanitizeArtifactFileSegment;
|
|
27
|
+
exports.usage = usage;
|
|
28
|
+
exports.validateArgentDriverSteps = validateArgentDriverSteps;
|
|
29
|
+
exports.writeArgentAvailabilityArtifacts = writeArgentAvailabilityArtifacts;
|
|
30
|
+
const { spawn } = require('node:child_process');
|
|
31
|
+
const crypto = require('node:crypto');
|
|
32
|
+
const fs = require('node:fs');
|
|
33
|
+
const fsp = require('node:fs/promises');
|
|
34
|
+
const path = require('node:path');
|
|
35
|
+
const { buildAgentSummaryMarkdown } = require('../core/agent-summary');
|
|
36
|
+
const { createArtifactLayout } = require('../core/artifact-layout');
|
|
37
|
+
const { writeJsonArtifact, writeTextArtifact } = require('../core/artifact-writer');
|
|
38
|
+
const { buildScenarioExecutionPlan } = require('../core/execution-plan');
|
|
39
|
+
const { SCHEMAS, assertValidJson } = require('../core/schema-validator');
|
|
40
|
+
const { hasHelpFlag, writeUsage } = require('./cli');
|
|
41
|
+
const { createArgentDriver, formatArgentRawOutput, isArgentRootOnlyDescription, } = require('./argent-driver');
|
|
42
|
+
const { createIosSimctlDriver, formatIosSimctlRawOutput, } = require('./ios-simctl-driver');
|
|
43
|
+
const { loadAslLocalEnv, readBooleanArgOrEnv, readStringArgOrEnv, } = require('./local-env');
|
|
44
|
+
const DASH_VALUE_KEYS = new Set(['app-flag', 'base-args', 'device-flag']);
|
|
45
|
+
const DEFAULT_ARGENT_BASE_ARGS = ['run'];
|
|
46
|
+
const DEFAULT_ARGENT_REQUIRED_TOOLS = [
|
|
47
|
+
'launch-app',
|
|
48
|
+
'open-url',
|
|
49
|
+
'describe',
|
|
50
|
+
'screenshot',
|
|
51
|
+
'gesture-tap',
|
|
52
|
+
'gesture-swipe',
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* Prints CLI usage.
|
|
56
|
+
*
|
|
57
|
+
* @param {{write: (message: string) => unknown}} output
|
|
58
|
+
* @returns {void}
|
|
59
|
+
*/
|
|
60
|
+
function usage(output = process.stderr) {
|
|
61
|
+
writeUsage([
|
|
62
|
+
'Usage: asl-argent --platform <ios|android> --scenario <path> --device <id> [--app <bundle-or-package>] [--out <dir>] [--run-id <id>]',
|
|
63
|
+
'',
|
|
64
|
+
'Executes scenario-declared launch and portable driver actions through the external Argent CLI.',
|
|
65
|
+
'Writes health.json, verdict.json, agent-summary.md, raw command transcripts, and screenshot captures.',
|
|
66
|
+
'Use --check --out <dir> to verify the configured Argent command and required tool surface and preserve availability artifacts.',
|
|
67
|
+
'Use --argent <binary> and --base-args "<args>" to adapt local Argent installs without bundling Argent.',
|
|
68
|
+
'Use --device-flag and --app-flag when your Argent command expects platform-specific flag names.',
|
|
69
|
+
'Use --command-timeout-ms <ms> to bound each external Argent invocation.',
|
|
70
|
+
'Use --ios-simctl-screenshot-fallback on iOS when simctl should provide screenshot evidence if Argent screenshot is unavailable.',
|
|
71
|
+
'Use --xcrun <path> to route the iOS simctl screenshot fallback through a specific xcrun binary.',
|
|
72
|
+
], output);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Parses `--key value` CLI arguments.
|
|
76
|
+
*
|
|
77
|
+
* @param {string[]} argv
|
|
78
|
+
* @returns {CliArgs}
|
|
79
|
+
*/
|
|
80
|
+
function parseArgs(argv) {
|
|
81
|
+
const args = {};
|
|
82
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
83
|
+
const token = argv[index];
|
|
84
|
+
if (token === '--') {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!token?.startsWith('--')) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const equalsIndex = token.indexOf('=');
|
|
91
|
+
if (equalsIndex > 2) {
|
|
92
|
+
args[token.slice(2, equalsIndex)] = token.slice(equalsIndex + 1);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const key = token.slice(2);
|
|
96
|
+
const value = argv[index + 1];
|
|
97
|
+
if (value && (!value.startsWith('--') || DASH_VALUE_KEYS.has(key))) {
|
|
98
|
+
args[key] = value;
|
|
99
|
+
index += 1;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
args[key] = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return args;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Reads and parses a JSON object from disk.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} filePath
|
|
111
|
+
* @returns {Record<string, unknown>}
|
|
112
|
+
*/
|
|
113
|
+
function readJson(filePath) {
|
|
114
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Reads a finite number from adapter metadata.
|
|
118
|
+
*
|
|
119
|
+
* @param {unknown} value
|
|
120
|
+
* @returns {number | undefined}
|
|
121
|
+
*/
|
|
122
|
+
function readFiniteNumber(value) {
|
|
123
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Reads a positive integer from CLI or scenario metadata.
|
|
127
|
+
*
|
|
128
|
+
* @param {unknown} value
|
|
129
|
+
* @param {number} fallback
|
|
130
|
+
* @returns {number}
|
|
131
|
+
*/
|
|
132
|
+
function readPositiveInteger(value, fallback) {
|
|
133
|
+
const parsed = typeof value === 'string' ? Number(value) : value;
|
|
134
|
+
return typeof parsed === 'number' && Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Creates a short random run id.
|
|
138
|
+
*
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
function createRunId() {
|
|
142
|
+
return crypto.randomBytes(6).toString('hex');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Runs a command and captures stdout, stderr, and exit code without throwing.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} command
|
|
148
|
+
* @param {string[]} args
|
|
149
|
+
* @returns {Promise<CommandResult>}
|
|
150
|
+
*/
|
|
151
|
+
function execFileCommand(command, args) {
|
|
152
|
+
return execFileCommandWithTimeout(command, args);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Runs a command with a bounded timeout and captures stdout, stderr, and exit code without throwing.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} command
|
|
158
|
+
* @param {string[]} args
|
|
159
|
+
* @param {number} [timeoutMs]
|
|
160
|
+
* @returns {Promise<CommandResult>}
|
|
161
|
+
*/
|
|
162
|
+
function execFileCommandWithTimeout(command, args, timeoutMs = 60_000) {
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
const child = spawn(command, args, {
|
|
165
|
+
detached: process.platform !== 'win32',
|
|
166
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
167
|
+
});
|
|
168
|
+
let stdout = '';
|
|
169
|
+
let stderr = '';
|
|
170
|
+
let settled = false;
|
|
171
|
+
let timedOut = false;
|
|
172
|
+
let forceKillTimer = null;
|
|
173
|
+
/**
|
|
174
|
+
* Signals the spawned command and its process group when the platform supports it.
|
|
175
|
+
*
|
|
176
|
+
* @param {NodeJS.Signals} signal
|
|
177
|
+
* @returns {void}
|
|
178
|
+
*/
|
|
179
|
+
const signalChildTree = (signal) => {
|
|
180
|
+
if (typeof child.pid !== 'number') {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
if (process.platform === 'win32') {
|
|
185
|
+
child.kill(signal);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
process.kill(-child.pid, signal);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
child.kill(signal);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const buildResult = (code, signal) => ({
|
|
196
|
+
command,
|
|
197
|
+
args,
|
|
198
|
+
exitCode: typeof code === 'number' ? code : signal ? 1 : 0,
|
|
199
|
+
stderr: [stderr, timedOut ? `Argent command timed out after ${timeoutMs}ms.` : ''].filter(Boolean).join('\n'),
|
|
200
|
+
stdout,
|
|
201
|
+
});
|
|
202
|
+
const timeout = setTimeout(() => {
|
|
203
|
+
timedOut = true;
|
|
204
|
+
signalChildTree('SIGTERM');
|
|
205
|
+
forceKillTimer = setTimeout(() => {
|
|
206
|
+
signalChildTree('SIGKILL');
|
|
207
|
+
finish(buildResult(null, 'SIGKILL'));
|
|
208
|
+
}, 1500);
|
|
209
|
+
}, timeoutMs);
|
|
210
|
+
const finish = (result) => {
|
|
211
|
+
if (settled) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
settled = true;
|
|
215
|
+
clearTimeout(timeout);
|
|
216
|
+
if (forceKillTimer) {
|
|
217
|
+
clearTimeout(forceKillTimer);
|
|
218
|
+
}
|
|
219
|
+
child.unref();
|
|
220
|
+
child.stdout?.unref();
|
|
221
|
+
child.stderr?.unref();
|
|
222
|
+
child.stdout?.destroy();
|
|
223
|
+
child.stderr?.destroy();
|
|
224
|
+
resolve(result);
|
|
225
|
+
};
|
|
226
|
+
child.stdout?.setEncoding('utf8');
|
|
227
|
+
child.stderr?.setEncoding('utf8');
|
|
228
|
+
child.stdout?.on('data', (chunk) => {
|
|
229
|
+
stdout += chunk;
|
|
230
|
+
});
|
|
231
|
+
child.stderr?.on('data', (chunk) => {
|
|
232
|
+
stderr += chunk;
|
|
233
|
+
});
|
|
234
|
+
child.on('error', (error) => {
|
|
235
|
+
finish({
|
|
236
|
+
command,
|
|
237
|
+
args,
|
|
238
|
+
exitCode: 1,
|
|
239
|
+
stderr: stderr || error.message,
|
|
240
|
+
stdout,
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
child.on('exit', (code, signal) => {
|
|
244
|
+
// `exit` can arrive before pipe data events drain, while `close` can wait on
|
|
245
|
+
// wrapper-spawned helpers that inherited stdio. Defer one tick to capture
|
|
246
|
+
// buffered output without reintroducing inherited-pipe hangs.
|
|
247
|
+
setImmediate(() => finish(buildResult(code, signal)));
|
|
248
|
+
});
|
|
249
|
+
child.on('close', (code, signal) => {
|
|
250
|
+
finish(buildResult(code, signal));
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Waits for the requested capture window.
|
|
256
|
+
*
|
|
257
|
+
* @param {number} ms
|
|
258
|
+
* @returns {Promise<void>}
|
|
259
|
+
*/
|
|
260
|
+
function delay(ms) {
|
|
261
|
+
return new Promise((resolve) => {
|
|
262
|
+
setTimeout(resolve, ms);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Reads the first booted iOS simulator UDID from `simctl` JSON.
|
|
267
|
+
*
|
|
268
|
+
* @param {string} stdout
|
|
269
|
+
* @returns {string | null}
|
|
270
|
+
*/
|
|
271
|
+
function parseBootedIosSimulatorUdid(stdout) {
|
|
272
|
+
try {
|
|
273
|
+
const parsed = JSON.parse(stdout);
|
|
274
|
+
for (const devices of Object.values(parsed.devices ?? {})) {
|
|
275
|
+
const booted = devices.find((device) => device.state === 'Booted' && typeof device.udid === 'string');
|
|
276
|
+
if (booted?.udid) {
|
|
277
|
+
return booted.udid;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Resolves Argent's iOS device id because Argent does not understand simctl's `booted` shorthand.
|
|
288
|
+
*
|
|
289
|
+
* @param {number} commandTimeoutMs
|
|
290
|
+
* @returns {Promise<string | null>}
|
|
291
|
+
*/
|
|
292
|
+
async function resolveBootedIosSimulatorUdid(commandTimeoutMs) {
|
|
293
|
+
const result = await execFileCommandWithTimeout('xcrun', ['simctl', 'list', 'devices', 'booted', '-j'], Math.min(commandTimeoutMs, 10_000));
|
|
294
|
+
if (result.exitCode !== 0) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
return parseBootedIosSimulatorUdid(result.stdout);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Resolves the device id that should be passed to Argent.
|
|
301
|
+
*
|
|
302
|
+
* @param {{commandTimeoutMs: number, deviceId: string, platform: 'android' | 'ios', resolveBootedIosSimulatorUdid?: () => Promise<string | null>}} options
|
|
303
|
+
* @returns {Promise<{deviceId: string, requestedDeviceId?: string}>}
|
|
304
|
+
*/
|
|
305
|
+
async function resolveArgentDeviceId({ commandTimeoutMs, deviceId, platform, resolveBootedIosSimulatorUdid: resolveBooted = () => resolveBootedIosSimulatorUdid(commandTimeoutMs), }) {
|
|
306
|
+
if (platform !== 'ios' || deviceId !== 'booted') {
|
|
307
|
+
return { deviceId };
|
|
308
|
+
}
|
|
309
|
+
const resolvedDeviceId = await resolveBooted();
|
|
310
|
+
return resolvedDeviceId ? { deviceId: resolvedDeviceId, requestedDeviceId: deviceId } : { deviceId };
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Returns adapter options for an Argent-backed step.
|
|
314
|
+
*
|
|
315
|
+
* @param {ScenarioExecutionStep} step
|
|
316
|
+
* @returns {Record<string, unknown>}
|
|
317
|
+
*/
|
|
318
|
+
function readArgentStepOptions(step) {
|
|
319
|
+
const argentOptions = step.adapterOptions?.argent;
|
|
320
|
+
return argentOptions && typeof argentOptions === 'object' && !Array.isArray(argentOptions)
|
|
321
|
+
? argentOptions
|
|
322
|
+
: {};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Returns true when a normalized step has a portable selector.
|
|
326
|
+
*
|
|
327
|
+
* @param {unknown} value
|
|
328
|
+
* @returns {value is import('./argent-driver').ArgentSelector}
|
|
329
|
+
*/
|
|
330
|
+
function isArgentSelector(value) {
|
|
331
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const selector = value;
|
|
335
|
+
return typeof selector.kind === 'string' && typeof selector.value === 'string' && selector.value.length > 0;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Returns the default raw file name for one Argent action.
|
|
339
|
+
*
|
|
340
|
+
* @param {{driverAction: ArgentDriverStep['driverAction'], index: number}} options
|
|
341
|
+
* @returns {string}
|
|
342
|
+
*/
|
|
343
|
+
function defaultArgentRawFileName({ driverAction, index, }) {
|
|
344
|
+
return `argent-${driverAction}-${index}.txt`;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Converts a scenario step id into a safe artifact filename segment.
|
|
348
|
+
*
|
|
349
|
+
* @param {string} value
|
|
350
|
+
* @returns {string}
|
|
351
|
+
*/
|
|
352
|
+
function sanitizeArtifactFileSegment(value) {
|
|
353
|
+
const sanitized = value.replace(/[^a-z0-9._-]+/giu, '-').replace(/^-+|-+$/gu, '');
|
|
354
|
+
return sanitized || 'step';
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Returns the simctl fallback screenshot filename for an Argent screenshot step.
|
|
358
|
+
*
|
|
359
|
+
* @param {ArgentDriverStep} driverStep
|
|
360
|
+
* @returns {string}
|
|
361
|
+
*/
|
|
362
|
+
function defaultIosSimctlFallbackScreenshotFileName(driverStep) {
|
|
363
|
+
return driverStep.captureFileName && driverStep.captureFileName.length > 0
|
|
364
|
+
? driverStep.captureFileName
|
|
365
|
+
: `ios-simctl-${sanitizeArtifactFileSegment(driverStep.stepId)}.png`;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Expands normalized scenario steps into Argent driver actions.
|
|
369
|
+
*
|
|
370
|
+
* @param {Record<string, unknown>} scenario
|
|
371
|
+
* @param {import('./argent-driver').ArgentScreenSize | undefined} screenSize
|
|
372
|
+
* @returns {ArgentDriverStep[]}
|
|
373
|
+
*/
|
|
374
|
+
function resolveArgentDriverSteps(scenario, screenSize) {
|
|
375
|
+
const executionPlan = buildScenarioExecutionPlan(scenario);
|
|
376
|
+
return executionPlan.steps
|
|
377
|
+
.filter((step) => step.kind === 'launch' ||
|
|
378
|
+
['assertVisible', 'inspectTree', 'screenshot', 'scroll', 'tap'].includes(String(step.driverAction)))
|
|
379
|
+
.map((step, index) => {
|
|
380
|
+
const argentOptions = readArgentStepOptions(step);
|
|
381
|
+
const action = step.kind === 'launch' ? 'launch' : step.driverAction;
|
|
382
|
+
const actionIndex = index + 1;
|
|
383
|
+
return {
|
|
384
|
+
driverAction: action,
|
|
385
|
+
...(typeof argentOptions.appId === 'string' ? { appId: argentOptions.appId } : {}),
|
|
386
|
+
...(typeof argentOptions.captureFileName === 'string' ? { captureFileName: argentOptions.captureFileName } : {}),
|
|
387
|
+
...(typeof readFiniteNumber(argentOptions.durationMs) === 'number'
|
|
388
|
+
? { durationMs: readFiniteNumber(argentOptions.durationMs) }
|
|
389
|
+
: {}),
|
|
390
|
+
...(typeof readFiniteNumber(argentOptions.endX) === 'number' ? { endX: readFiniteNumber(argentOptions.endX) } : {}),
|
|
391
|
+
...(typeof readFiniteNumber(argentOptions.endY) === 'number' ? { endY: readFiniteNumber(argentOptions.endY) } : {}),
|
|
392
|
+
rawFileName: typeof argentOptions.rawFileName === 'string' && argentOptions.rawFileName.length > 0
|
|
393
|
+
? argentOptions.rawFileName
|
|
394
|
+
: defaultArgentRawFileName({ driverAction: action, index: actionIndex }),
|
|
395
|
+
required: step.required !== false,
|
|
396
|
+
...(screenSize ? { screenSize } : {}),
|
|
397
|
+
...(isArgentSelector(argentOptions.selector)
|
|
398
|
+
? { selector: argentOptions.selector }
|
|
399
|
+
: isArgentSelector(step.selector)
|
|
400
|
+
? { selector: step.selector }
|
|
401
|
+
: {}),
|
|
402
|
+
...(typeof readFiniteNumber(argentOptions.startX) === 'number' ? { startX: readFiniteNumber(argentOptions.startX) } : {}),
|
|
403
|
+
...(typeof readFiniteNumber(argentOptions.startY) === 'number' ? { startY: readFiniteNumber(argentOptions.startY) } : {}),
|
|
404
|
+
stepId: step.id,
|
|
405
|
+
waitMs: readPositiveInteger(argentOptions.waitMs ?? step.timeoutMs, 0),
|
|
406
|
+
...(typeof readFiniteNumber(argentOptions.x) === 'number' ? { x: readFiniteNumber(argentOptions.x) } : {}),
|
|
407
|
+
...(typeof readFiniteNumber(argentOptions.y) === 'number' ? { y: readFiniteNumber(argentOptions.y) } : {}),
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Returns profile-time validation errors for Argent driver steps.
|
|
413
|
+
*
|
|
414
|
+
* @param {ArgentDriverStep[]} driverSteps
|
|
415
|
+
* @param {{app?: string | null}} options
|
|
416
|
+
* @returns {string[]}
|
|
417
|
+
*/
|
|
418
|
+
function validateArgentDriverSteps(driverSteps, options = {}) {
|
|
419
|
+
const errors = [];
|
|
420
|
+
for (const step of driverSteps) {
|
|
421
|
+
const stepLabel = step.stepId ? `step \`${step.stepId}\`` : 'unnamed step';
|
|
422
|
+
if (step.driverAction === 'launch' && !step.appId && !options.app) {
|
|
423
|
+
errors.push(`${stepLabel} is a launch step but no app id was provided through --app or adapterOptions.argent.appId.`);
|
|
424
|
+
}
|
|
425
|
+
if (step.driverAction === 'tap' && (typeof step.x !== 'number' || typeof step.y !== 'number')) {
|
|
426
|
+
errors.push(`${stepLabel} uses driverAction \`tap\` but is missing adapterOptions.argent.x/y.`);
|
|
427
|
+
}
|
|
428
|
+
if (step.driverAction === 'scroll' && (typeof step.startX !== 'number' ||
|
|
429
|
+
typeof step.startY !== 'number' ||
|
|
430
|
+
typeof step.endX !== 'number' ||
|
|
431
|
+
typeof step.endY !== 'number')) {
|
|
432
|
+
errors.push(`${stepLabel} uses driverAction \`scroll\` but is missing adapterOptions.argent.startX/startY/endX/endY.`);
|
|
433
|
+
}
|
|
434
|
+
if (step.driverAction === 'assertVisible' && !step.selector) {
|
|
435
|
+
errors.push(`${stepLabel} uses driverAction \`assertVisible\` but is missing a portable selector.`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return errors;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Builds scalar health metadata for one portable selector.
|
|
442
|
+
*
|
|
443
|
+
* @param {import('./argent-driver').ArgentSelector | undefined} selector
|
|
444
|
+
* @returns {Record<string, string>}
|
|
445
|
+
*/
|
|
446
|
+
function buildArgentSelectorHealthMetadata(selector) {
|
|
447
|
+
if (!selector) {
|
|
448
|
+
return {};
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
selectorKind: selector.kind,
|
|
452
|
+
...(selector.match ? { selectorMatch: selector.match } : {}),
|
|
453
|
+
selectorValue: selector.value,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Builds the most specific next-action hint available from an Argent failure.
|
|
458
|
+
*
|
|
459
|
+
* @param {ArgentFailureHintOptions} options
|
|
460
|
+
* @returns {{nextAction: string, nextActionCode: string, argentDiagnostic?: string}}
|
|
461
|
+
*/
|
|
462
|
+
function buildArgentFailureMetadata({ driverAction, fallbackCapturePath, missingRequiredScreenshot, rawFileName, result, rootOnlyDescription, }) {
|
|
463
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
464
|
+
if (/ENOENT|command not found|not found/iu.test(output)) {
|
|
465
|
+
return {
|
|
466
|
+
argentDiagnostic: 'argent_command_unavailable',
|
|
467
|
+
nextAction: 'Install Argent, pass --argent with the local Argent command, or set --base-args/--device-flag/--app-flag to match the installed command before rerunning.',
|
|
468
|
+
nextActionCode: 'configure_argent_command',
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
if (/timed out after \d+ms/iu.test(output)) {
|
|
472
|
+
return {
|
|
473
|
+
argentDiagnostic: 'argent_command_timeout',
|
|
474
|
+
nextAction: 'Confirm the Argent command can run without package-manager or device-control prompts, increase --command-timeout-ms if the command is legitimately slow, then rerun.',
|
|
475
|
+
nextActionCode: 'fix_argent_command_timeout',
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (/SimulatorServer|simulator-server/iu.test(output)) {
|
|
479
|
+
return {
|
|
480
|
+
argentDiagnostic: 'argent_simulator_server_unavailable',
|
|
481
|
+
...(fallbackCapturePath ? { fallbackCapturePath, fallbackProvider: 'ios-simctl' } : {}),
|
|
482
|
+
nextAction: fallbackCapturePath
|
|
483
|
+
? `Argent could not start its simulator-server dependency for ${driverAction}, but iOS simctl fallback captured ${fallbackCapturePath}. Inspect raw/${rawFileName} before relying on Argent screenshot evidence.`
|
|
484
|
+
: `Argent could not start its simulator-server dependency for ${driverAction}. Inspect raw/${rawFileName}, verify the selected simulator is accessible to Argent, and use simctl or another screenshot provider when screenshot evidence is required.`,
|
|
485
|
+
nextActionCode: 'fix_argent_simulator_server',
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (rootOnlyDescription) {
|
|
489
|
+
return {
|
|
490
|
+
argentDiagnostic: 'root_only_description',
|
|
491
|
+
nextAction: `Argent returned only the root UI description for ${driverAction}. Inspect raw/${rawFileName}, confirm the app is foregrounded and visible to Argent, then rerun.`,
|
|
492
|
+
nextActionCode: 'fix_argent_visibility_target',
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
if (missingRequiredScreenshot) {
|
|
496
|
+
return {
|
|
497
|
+
argentDiagnostic: 'missing_screenshot_path',
|
|
498
|
+
nextAction: `Argent completed screenshot without reporting a saved file. Inspect raw/${rawFileName}, adjust the Argent command shape, or make the screenshot step optional before rerunning.`,
|
|
499
|
+
nextActionCode: 'fix_argent_screenshot_output',
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
nextAction: `Inspect raw/${rawFileName}, confirm Argent can see the selected app/device, and rerun the capture.`,
|
|
504
|
+
nextActionCode: 'inspect_argent_driver_action',
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Runs one Argent driver action.
|
|
509
|
+
*
|
|
510
|
+
* @param {{driver: import('./argent-driver').ArgentDriver, driverStep: ArgentDriverStep}} options
|
|
511
|
+
* @returns {Promise<import('./argent-driver').ArgentCommandResult>}
|
|
512
|
+
*/
|
|
513
|
+
async function runArgentDriverStep({ driver, driverStep, }) {
|
|
514
|
+
if (driverStep.driverAction === 'launch') {
|
|
515
|
+
return driver.launchApp({
|
|
516
|
+
...(driverStep.appId ? { appId: driverStep.appId } : {}),
|
|
517
|
+
rawFileName: driverStep.rawFileName,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
if (driverStep.driverAction === 'assertVisible' && driverStep.selector) {
|
|
521
|
+
return driver.assertVisible({
|
|
522
|
+
...(driverStep.appId ? { appId: driverStep.appId } : {}),
|
|
523
|
+
rawFileName: driverStep.rawFileName,
|
|
524
|
+
selector: driverStep.selector,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (driverStep.driverAction === 'inspectTree') {
|
|
528
|
+
return driver.inspectTree({
|
|
529
|
+
...(driverStep.appId ? { appId: driverStep.appId } : {}),
|
|
530
|
+
rawFileName: driverStep.rawFileName,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (driverStep.driverAction === 'screenshot') {
|
|
534
|
+
return driver.screenshot({
|
|
535
|
+
rawFileName: driverStep.rawFileName,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
if (driverStep.driverAction === 'scroll') {
|
|
539
|
+
return driver.scroll({
|
|
540
|
+
...(typeof driverStep.durationMs === 'number' ? { durationMs: driverStep.durationMs } : {}),
|
|
541
|
+
endX: driverStep.endX,
|
|
542
|
+
endY: driverStep.endY,
|
|
543
|
+
rawFileName: driverStep.rawFileName,
|
|
544
|
+
...(driverStep.screenSize ? { screenSize: driverStep.screenSize } : {}),
|
|
545
|
+
startX: driverStep.startX,
|
|
546
|
+
startY: driverStep.startY,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
if (driverStep.driverAction === 'tap') {
|
|
550
|
+
return driver.tap({
|
|
551
|
+
rawFileName: driverStep.rawFileName,
|
|
552
|
+
...(driverStep.screenSize ? { screenSize: driverStep.screenSize } : {}),
|
|
553
|
+
x: driverStep.x,
|
|
554
|
+
y: driverStep.y,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
throw new Error(`Unsupported Argent driver action: ${driverStep.driverAction}`);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Captures an iOS screenshot through simctl when Argent's iOS screenshot backend is unavailable.
|
|
561
|
+
*
|
|
562
|
+
* @param {{capturesDir: string, deviceId: string, driverStep: ArgentDriverStep, executor?: CommandExecutor, xcrunPath: string}} options
|
|
563
|
+
* @returns {Promise<IosSimctlScreenshotFallbackResult>}
|
|
564
|
+
*/
|
|
565
|
+
async function runIosSimctlScreenshotFallback({ capturesDir, deviceId, driverStep, executor = execFileCommandWithTimeout, xcrunPath, }) {
|
|
566
|
+
const fileName = defaultIosSimctlFallbackScreenshotFileName(driverStep);
|
|
567
|
+
const outputPath = path.join(capturesDir, fileName);
|
|
568
|
+
const rawFileName = `ios-simctl-${sanitizeArtifactFileSegment(driverStep.stepId)}-screenshot.txt`;
|
|
569
|
+
const driver = createIosSimctlDriver({
|
|
570
|
+
deviceUdid: deviceId,
|
|
571
|
+
executor,
|
|
572
|
+
xcrunPath,
|
|
573
|
+
});
|
|
574
|
+
const result = await driver.screenshot({
|
|
575
|
+
outputPath,
|
|
576
|
+
rawFileName,
|
|
577
|
+
});
|
|
578
|
+
try {
|
|
579
|
+
await fsp.access(outputPath);
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
return {
|
|
583
|
+
rawFileName,
|
|
584
|
+
result,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
capturePath: `captures/${fileName}`,
|
|
589
|
+
rawFileName,
|
|
590
|
+
result,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Builds a stable health code suffix for one Argent driver action.
|
|
595
|
+
*
|
|
596
|
+
* @param {ArgentDriverStep['driverAction']} driverAction
|
|
597
|
+
* @returns {string}
|
|
598
|
+
*/
|
|
599
|
+
function argentDriverActionCode(driverAction) {
|
|
600
|
+
return driverAction.replace(/[A-Z]/gu, (match) => `_${match.toLowerCase()}`);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Builds a health artifact from Argent capture checks.
|
|
604
|
+
*
|
|
605
|
+
* @param {{flowId?: string, runId: string, scenarioId: string, checks: Record<string, unknown>[]}} options
|
|
606
|
+
* @returns {Record<string, unknown>}
|
|
607
|
+
*/
|
|
608
|
+
function buildArgentHealth({ checks, flowId, runId, scenarioId, }) {
|
|
609
|
+
const failed = checks.some((check) => check.status === 'failed');
|
|
610
|
+
return assertValidJson({
|
|
611
|
+
schemaVersion: '1.0.0',
|
|
612
|
+
scenarioId,
|
|
613
|
+
...(flowId ? { flowId } : {}),
|
|
614
|
+
runId,
|
|
615
|
+
healthStatus: failed ? 'failed' : 'passed',
|
|
616
|
+
checks,
|
|
617
|
+
}, SCHEMAS.health, 'Health artifact');
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Builds a verdict artifact for Argent capture readiness.
|
|
621
|
+
*
|
|
622
|
+
* @param {{health: Record<string, unknown>, runId: string, scenarioId: string, flowId?: string}} options
|
|
623
|
+
* @returns {Record<string, unknown>}
|
|
624
|
+
*/
|
|
625
|
+
function buildArgentVerdict({ flowId, health, runId, scenarioId, }) {
|
|
626
|
+
const passed = health.healthStatus === 'passed';
|
|
627
|
+
return assertValidJson({
|
|
628
|
+
schemaVersion: '1.0.0',
|
|
629
|
+
scenarioId,
|
|
630
|
+
...(flowId ? { flowId } : {}),
|
|
631
|
+
runId,
|
|
632
|
+
healthStatus: health.healthStatus,
|
|
633
|
+
verdictStatus: passed ? 'not_evaluated' : 'inconclusive',
|
|
634
|
+
budgetChecks: [],
|
|
635
|
+
summary: passed
|
|
636
|
+
? 'Argent capture passed; no product budget has been evaluated.'
|
|
637
|
+
: 'Argent capture failed; runtime scenario execution is not ready.',
|
|
638
|
+
}, SCHEMAS.verdict, 'Verdict artifact');
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Splits CLI base args without invoking a shell.
|
|
642
|
+
*
|
|
643
|
+
* @param {unknown} value
|
|
644
|
+
* @returns {string[] | undefined}
|
|
645
|
+
*/
|
|
646
|
+
function parseBaseArgs(value) {
|
|
647
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
return value.trim().split(/\s+/u);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Returns root-level Argent args from an `argent run` command shape.
|
|
654
|
+
*
|
|
655
|
+
* @param {string[]} baseArgs
|
|
656
|
+
* @returns {string[]}
|
|
657
|
+
*/
|
|
658
|
+
function deriveArgentRootArgs(baseArgs) {
|
|
659
|
+
return baseArgs.at(-1) === 'run' ? baseArgs.slice(0, -1) : baseArgs;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Converts one Argent availability check into a schema-safe health check.
|
|
663
|
+
*
|
|
664
|
+
* @param {ArgentAvailabilityCheck} check
|
|
665
|
+
* @returns {Record<string, unknown>}
|
|
666
|
+
*/
|
|
667
|
+
function argentAvailabilityHealthCheck(check) {
|
|
668
|
+
return {
|
|
669
|
+
name: check.name,
|
|
670
|
+
status: check.status,
|
|
671
|
+
source: 'runner',
|
|
672
|
+
code: check.code,
|
|
673
|
+
message: check.message,
|
|
674
|
+
metadata: {
|
|
675
|
+
command: check.command,
|
|
676
|
+
args: check.args.join(' '),
|
|
677
|
+
exitCode: check.exitCode,
|
|
678
|
+
...(check.stderrPreview ? { stderrPreview: check.stderrPreview } : {}),
|
|
679
|
+
...(check.stdoutPreview ? { stdoutPreview: check.stdoutPreview } : {}),
|
|
680
|
+
...(check.metadata ?? {}),
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Writes ASL artifacts for an Argent command-surface availability check.
|
|
686
|
+
*
|
|
687
|
+
* @param {ArgentAvailabilityArtifactOptions} options
|
|
688
|
+
* @returns {Promise<{agentSummary: string, health: Record<string, unknown>, runDir: string, verdict: Record<string, unknown>}>}
|
|
689
|
+
*/
|
|
690
|
+
async function writeArgentAvailabilityArtifacts({ outputDir, result, runId = createRunId(), }) {
|
|
691
|
+
const runDir = path.resolve(outputDir);
|
|
692
|
+
const layout = createArtifactLayout({ outputDir: runDir });
|
|
693
|
+
const checks = result.checks.map(argentAvailabilityHealthCheck);
|
|
694
|
+
const health = buildArgentHealth({
|
|
695
|
+
checks,
|
|
696
|
+
flowId: 'argent-availability',
|
|
697
|
+
runId,
|
|
698
|
+
scenarioId: 'argent-availability',
|
|
699
|
+
});
|
|
700
|
+
const verdict = assertValidJson({
|
|
701
|
+
schemaVersion: '1.0.0',
|
|
702
|
+
scenarioId: 'argent-availability',
|
|
703
|
+
flowId: 'argent-availability',
|
|
704
|
+
runId,
|
|
705
|
+
healthStatus: health.healthStatus,
|
|
706
|
+
verdictStatus: result.status === 'passed' ? 'not_evaluated' : 'inconclusive',
|
|
707
|
+
budgetChecks: [],
|
|
708
|
+
summary: result.status === 'passed'
|
|
709
|
+
? 'Argent command surface is available; no product budget has been evaluated.'
|
|
710
|
+
: 'Argent command surface is unavailable; fix runner environment health before live proof.',
|
|
711
|
+
}, SCHEMAS.verdict, 'Verdict artifact');
|
|
712
|
+
const agentSummary = buildAgentSummaryMarkdown({ health, verdict });
|
|
713
|
+
await fsp.mkdir(layout.raw, { recursive: true });
|
|
714
|
+
await writeJsonArtifact({
|
|
715
|
+
filePath: layout.health,
|
|
716
|
+
value: health,
|
|
717
|
+
schema: SCHEMAS.health,
|
|
718
|
+
label: 'Health artifact',
|
|
719
|
+
});
|
|
720
|
+
await writeJsonArtifact({
|
|
721
|
+
filePath: layout.verdict,
|
|
722
|
+
value: verdict,
|
|
723
|
+
schema: SCHEMAS.verdict,
|
|
724
|
+
label: 'Verdict artifact',
|
|
725
|
+
});
|
|
726
|
+
await writeTextArtifact({
|
|
727
|
+
filePath: layout.agentSummary,
|
|
728
|
+
content: agentSummary,
|
|
729
|
+
});
|
|
730
|
+
await fsp.writeFile(path.join(layout.raw, 'argent-availability.json'), `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
|
731
|
+
return {
|
|
732
|
+
agentSummary,
|
|
733
|
+
health,
|
|
734
|
+
runDir,
|
|
735
|
+
verdict,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Returns a compact single-line preview for command diagnostics.
|
|
740
|
+
*
|
|
741
|
+
* @param {string} value
|
|
742
|
+
* @returns {string | undefined}
|
|
743
|
+
*/
|
|
744
|
+
function previewCommandOutput(value) {
|
|
745
|
+
const preview = value.replace(/\s+/gu, ' ').trim();
|
|
746
|
+
return preview.length > 240 ? `${preview.slice(0, 237)}...` : preview || undefined;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Classifies an Argent availability failure into the next operational step.
|
|
750
|
+
*
|
|
751
|
+
* @param {CommandResult} result
|
|
752
|
+
* @returns {Record<string, string>}
|
|
753
|
+
*/
|
|
754
|
+
function classifyArgentAvailabilityFailure(result) {
|
|
755
|
+
const diagnostic = `${result.stderr}\n${result.stdout}`;
|
|
756
|
+
if (/operation not permitted|permission denied|sandbox|eacces|eperm|cannot bind|smartsocket/iu.test(diagnostic)) {
|
|
757
|
+
return {
|
|
758
|
+
failureClass: 'host_access',
|
|
759
|
+
nextAction: 'Rerun Argent availability with host/device access before treating this as an app, scenario, or runner regression.',
|
|
760
|
+
nextActionCode: 'rerun_with_host_access',
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
if (/timed out|timeout/iu.test(diagnostic)) {
|
|
764
|
+
return {
|
|
765
|
+
failureClass: 'timeout',
|
|
766
|
+
nextAction: 'Confirm Argent can run without prompts, use a direct Argent binary when available, or increase --command-timeout-ms before rerunning the availability check.',
|
|
767
|
+
nextActionCode: 'increase_argent_timeout',
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
if (/enoent|not found|command not found|no such file or directory|could not determine executable/iu.test(diagnostic)) {
|
|
771
|
+
return {
|
|
772
|
+
failureClass: 'missing_binary',
|
|
773
|
+
nextAction: 'Install Argent, pass the correct binary with --argent, or provide the wrapper shape with --base-args before starting live proof.',
|
|
774
|
+
nextActionCode: 'configure_argent_binary',
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
failureClass: 'command_surface',
|
|
779
|
+
nextAction: 'Inspect the failed Argent command output, fix the command surface, then rerun the availability check before starting live proof.',
|
|
780
|
+
nextActionCode: 'inspect_argent_availability',
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Builds one availability check result from an Argent command execution.
|
|
785
|
+
*
|
|
786
|
+
* @param {{code: string, expectedPattern: RegExp, name: string, result: CommandResult}} options
|
|
787
|
+
* @returns {ArgentAvailabilityCheck}
|
|
788
|
+
*/
|
|
789
|
+
function buildArgentAvailabilityCheck({ code, expectedPattern, name, result, }) {
|
|
790
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
791
|
+
const expectedOutputFound = expectedPattern.test(output);
|
|
792
|
+
const completedBeforeWrapperTimeout = expectedOutputFound && /timed out after \d+ms/iu.test(result.stderr);
|
|
793
|
+
const passed = expectedOutputFound && (result.exitCode === 0 || completedBeforeWrapperTimeout);
|
|
794
|
+
const check = {
|
|
795
|
+
args: result.args,
|
|
796
|
+
code,
|
|
797
|
+
command: result.command,
|
|
798
|
+
exitCode: result.exitCode,
|
|
799
|
+
message: passed
|
|
800
|
+
? completedBeforeWrapperTimeout
|
|
801
|
+
? `${name} returned the expected Argent output before a wrapper timeout.`
|
|
802
|
+
: `${name} is available.`
|
|
803
|
+
: `${name} did not return the expected Argent output.`,
|
|
804
|
+
name,
|
|
805
|
+
status: passed ? 'passed' : 'failed',
|
|
806
|
+
};
|
|
807
|
+
if (!passed) {
|
|
808
|
+
const stderrPreview = previewCommandOutput(result.stderr);
|
|
809
|
+
const stdoutPreview = previewCommandOutput(result.stdout);
|
|
810
|
+
check.metadata = classifyArgentAvailabilityFailure(result);
|
|
811
|
+
if (stderrPreview) {
|
|
812
|
+
check.stderrPreview = stderrPreview;
|
|
813
|
+
}
|
|
814
|
+
if (stdoutPreview) {
|
|
815
|
+
check.stdoutPreview = stdoutPreview;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return check;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Verifies that the configured Argent command can invoke the ASL-required tool surface.
|
|
822
|
+
*
|
|
823
|
+
* @param {ArgentAvailabilityOptions} options
|
|
824
|
+
* @returns {Promise<ArgentAvailabilityResult>}
|
|
825
|
+
*/
|
|
826
|
+
async function checkArgentAvailability({ argentCommand = 'argent', baseArgs = DEFAULT_ARGENT_BASE_ARGS, commandTimeoutMs = 30_000, executor, requiredTools = DEFAULT_ARGENT_REQUIRED_TOOLS, } = {}) {
|
|
827
|
+
const run = executor ?? ((command, args) => execFileCommandWithTimeout(command, args, commandTimeoutMs));
|
|
828
|
+
const checks = [];
|
|
829
|
+
const runHelp = await run(argentCommand, [...baseArgs, '--help']);
|
|
830
|
+
checks.push(buildArgentAvailabilityCheck({
|
|
831
|
+
code: 'argent_run_help_available',
|
|
832
|
+
expectedPattern: /Usage:\s+argent\s+run\s+<tool>/iu,
|
|
833
|
+
name: 'argent_run_help',
|
|
834
|
+
result: runHelp,
|
|
835
|
+
}));
|
|
836
|
+
const rootArgs = deriveArgentRootArgs(baseArgs);
|
|
837
|
+
for (const tool of requiredTools) {
|
|
838
|
+
const result = await run(argentCommand, [...rootArgs, 'tools', 'describe', tool]);
|
|
839
|
+
checks.push(buildArgentAvailabilityCheck({
|
|
840
|
+
code: `argent_tool_${tool.replace(/-/gu, '_')}_available`,
|
|
841
|
+
expectedPattern: new RegExp(`Tool:\\s+${tool.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')}`, 'iu'),
|
|
842
|
+
name: `argent_tool_${tool}`,
|
|
843
|
+
result,
|
|
844
|
+
}));
|
|
845
|
+
}
|
|
846
|
+
const status = checks.every((check) => check.status === 'passed') ? 'passed' : 'failed';
|
|
847
|
+
return {
|
|
848
|
+
argentCommand,
|
|
849
|
+
baseArgs,
|
|
850
|
+
checks,
|
|
851
|
+
requiredTools,
|
|
852
|
+
status,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Returns a screen size from CLI values when both dimensions are present.
|
|
857
|
+
*
|
|
858
|
+
* @param {{width?: unknown, height?: unknown}} options
|
|
859
|
+
* @returns {import('./argent-driver').ArgentScreenSize | undefined}
|
|
860
|
+
*/
|
|
861
|
+
function readScreenSize({ height, width, }) {
|
|
862
|
+
const parsedWidth = typeof width === 'string' ? Number(width) : width;
|
|
863
|
+
const parsedHeight = typeof height === 'string' ? Number(height) : height;
|
|
864
|
+
if (typeof parsedWidth !== 'number' || typeof parsedHeight !== 'number') {
|
|
865
|
+
return undefined;
|
|
866
|
+
}
|
|
867
|
+
if (!Number.isFinite(parsedWidth) || !Number.isFinite(parsedHeight) || parsedWidth <= 0 || parsedHeight <= 0) {
|
|
868
|
+
throw new Error('--screen-width and --screen-height must be positive numbers.');
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
height: parsedHeight,
|
|
872
|
+
width: parsedWidth,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Copies a screenshot produced by Argent into the stable ASL captures folder.
|
|
877
|
+
*
|
|
878
|
+
* @param {{capturesDir: string, capturePath: string, preferredFileName?: string}} options
|
|
879
|
+
* @returns {Promise<string | null>}
|
|
880
|
+
*/
|
|
881
|
+
async function copyArgentCapture({ capturePath, capturesDir, preferredFileName, }) {
|
|
882
|
+
try {
|
|
883
|
+
await fsp.access(capturePath);
|
|
884
|
+
}
|
|
885
|
+
catch {
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
const fileName = preferredFileName && preferredFileName.length > 0
|
|
889
|
+
? preferredFileName
|
|
890
|
+
: path.basename(capturePath);
|
|
891
|
+
const destination = path.join(capturesDir, fileName);
|
|
892
|
+
if (path.resolve(capturePath) !== path.resolve(destination)) {
|
|
893
|
+
await fsp.copyFile(capturePath, destination);
|
|
894
|
+
}
|
|
895
|
+
return `captures/${fileName}`;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Runs scenario-declared portable actions through Argent and writes ASL artifacts.
|
|
899
|
+
*
|
|
900
|
+
* @param {ArgentCaptureOptions} options
|
|
901
|
+
* @returns {Promise<ArgentCaptureResult>}
|
|
902
|
+
*/
|
|
903
|
+
async function runArgentCapture({ app = null, appFlag, argentCommand = 'argent', baseArgs, commandTimeoutMs = 60_000, delay: wait = delay, deviceFlag, deviceId, executor, iosSimctlExecutor, iosSimctlScreenshotFallback = false, outputDir = path.resolve('artifacts/argent-capture'), platform, resolveBootedIosSimulatorUdid: resolveBooted, runId = createRunId(), scenario, screenSize, waitMs = 0, xcrunPath = 'xcrun', }) {
|
|
904
|
+
const runDir = path.resolve(outputDir);
|
|
905
|
+
const layout = createArtifactLayout({ outputDir: runDir });
|
|
906
|
+
const rawDir = layout.raw;
|
|
907
|
+
await fsp.mkdir(rawDir, { recursive: true });
|
|
908
|
+
await fsp.mkdir(layout.captures, { recursive: true });
|
|
909
|
+
const executionPlan = buildScenarioExecutionPlan(scenario);
|
|
910
|
+
const raw = {};
|
|
911
|
+
const captures = {
|
|
912
|
+
screenshots: [],
|
|
913
|
+
};
|
|
914
|
+
const checks = [];
|
|
915
|
+
const driverActionMetadata = [];
|
|
916
|
+
const resolvedDriverSteps = resolveArgentDriverSteps(scenario, screenSize);
|
|
917
|
+
const driverStepErrors = validateArgentDriverSteps(resolvedDriverSteps, { app });
|
|
918
|
+
if (driverStepErrors.length > 0) {
|
|
919
|
+
throw new Error(`Invalid Argent driver step metadata: ${driverStepErrors.join(' ')}`);
|
|
920
|
+
}
|
|
921
|
+
const resolvedDevice = await resolveArgentDeviceId({
|
|
922
|
+
commandTimeoutMs,
|
|
923
|
+
deviceId,
|
|
924
|
+
platform,
|
|
925
|
+
...(resolveBooted ? { resolveBootedIosSimulatorUdid: resolveBooted } : {}),
|
|
926
|
+
});
|
|
927
|
+
const driver = createArgentDriver({
|
|
928
|
+
...(app ? { appId: app } : {}),
|
|
929
|
+
...(appFlag ? { appFlag } : {}),
|
|
930
|
+
argentCommand,
|
|
931
|
+
...(baseArgs ? { baseArgs } : {}),
|
|
932
|
+
...(deviceFlag ? { deviceFlag } : {}),
|
|
933
|
+
deviceId: resolvedDevice.deviceId,
|
|
934
|
+
executor: executor ?? ((command, args) => execFileCommandWithTimeout(command, args, commandTimeoutMs)),
|
|
935
|
+
...(screenSize ? { screenSize } : {}),
|
|
936
|
+
});
|
|
937
|
+
const metadata = {
|
|
938
|
+
app,
|
|
939
|
+
argentCommand,
|
|
940
|
+
baseArgs: baseArgs ?? ['run'],
|
|
941
|
+
captures,
|
|
942
|
+
commandTimeoutMs,
|
|
943
|
+
deviceId: resolvedDevice.deviceId,
|
|
944
|
+
driverActions: [],
|
|
945
|
+
platform,
|
|
946
|
+
...(resolvedDevice.requestedDeviceId ? { requestedDeviceId: resolvedDevice.requestedDeviceId } : {}),
|
|
947
|
+
...(screenSize ? { screenSize } : {}),
|
|
948
|
+
};
|
|
949
|
+
if (waitMs > 0) {
|
|
950
|
+
await wait(waitMs);
|
|
951
|
+
checks.push({
|
|
952
|
+
name: 'argent_capture_window_waited',
|
|
953
|
+
status: 'passed',
|
|
954
|
+
source: 'runner',
|
|
955
|
+
code: 'argent_capture_window_waited',
|
|
956
|
+
message: `Waited ${waitMs}ms before running Argent driver actions.`,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
for (const driverStep of resolvedDriverSteps) {
|
|
960
|
+
if (driverStep.waitMs > 0) {
|
|
961
|
+
await wait(driverStep.waitMs);
|
|
962
|
+
checks.push({
|
|
963
|
+
name: 'argent_driver_action_waited',
|
|
964
|
+
status: 'passed',
|
|
965
|
+
source: 'runner',
|
|
966
|
+
code: 'argent_driver_action_waited',
|
|
967
|
+
message: `Waited ${driverStep.waitMs}ms before running Argent driver action ${driverStep.driverAction}.`,
|
|
968
|
+
metadata: {
|
|
969
|
+
driverAction: driverStep.driverAction,
|
|
970
|
+
stepId: driverStep.stepId,
|
|
971
|
+
},
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
const driverResult = await runArgentDriverStep({ driver, driverStep });
|
|
975
|
+
raw[driverResult.rawFileName] = formatArgentRawOutput(driverResult);
|
|
976
|
+
const rootOnlyDescription = ['assertVisible', 'inspectTree'].includes(driverStep.driverAction) &&
|
|
977
|
+
isArgentRootOnlyDescription(driverResult.stdout);
|
|
978
|
+
const missingRequiredScreenshot = driverStep.driverAction === 'screenshot' &&
|
|
979
|
+
driverResult.exitCode === 0 &&
|
|
980
|
+
!driverResult.capturePath &&
|
|
981
|
+
driverStep.required;
|
|
982
|
+
const failed = driverResult.exitCode !== 0 || rootOnlyDescription || missingRequiredScreenshot;
|
|
983
|
+
const codeSuffix = argentDriverActionCode(driverStep.driverAction);
|
|
984
|
+
let stableCapturePath = null;
|
|
985
|
+
let fallbackCapture = null;
|
|
986
|
+
if (driverStep.driverAction === 'screenshot' && driverResult.exitCode === 0 && driverResult.capturePath) {
|
|
987
|
+
stableCapturePath = await copyArgentCapture({
|
|
988
|
+
capturePath: driverResult.capturePath,
|
|
989
|
+
capturesDir: layout.captures,
|
|
990
|
+
...(driverStep.captureFileName ? { preferredFileName: driverStep.captureFileName } : {}),
|
|
991
|
+
});
|
|
992
|
+
if (stableCapturePath) {
|
|
993
|
+
captures.screenshots.push(stableCapturePath);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (driverStep.driverAction === 'screenshot' &&
|
|
997
|
+
failed &&
|
|
998
|
+
platform === 'ios' &&
|
|
999
|
+
iosSimctlScreenshotFallback) {
|
|
1000
|
+
fallbackCapture = await runIosSimctlScreenshotFallback({
|
|
1001
|
+
capturesDir: layout.captures,
|
|
1002
|
+
deviceId: resolvedDevice.deviceId,
|
|
1003
|
+
driverStep,
|
|
1004
|
+
...(iosSimctlExecutor ? { executor: iosSimctlExecutor } : {}),
|
|
1005
|
+
xcrunPath,
|
|
1006
|
+
});
|
|
1007
|
+
raw[fallbackCapture.rawFileName] = formatIosSimctlRawOutput(fallbackCapture.result);
|
|
1008
|
+
if (fallbackCapture.capturePath) {
|
|
1009
|
+
stableCapturePath = fallbackCapture.capturePath;
|
|
1010
|
+
captures.screenshots.push(fallbackCapture.capturePath);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
const recoveredByFallback = Boolean(fallbackCapture?.capturePath);
|
|
1014
|
+
const status = failed && (driverStep.required === false || recoveredByFallback)
|
|
1015
|
+
? 'warning'
|
|
1016
|
+
: failed
|
|
1017
|
+
? 'failed'
|
|
1018
|
+
: 'passed';
|
|
1019
|
+
checks.push({
|
|
1020
|
+
name: `argent_${codeSuffix}`,
|
|
1021
|
+
status,
|
|
1022
|
+
source: 'runner',
|
|
1023
|
+
code: status === 'passed' ? `argent_${codeSuffix}_completed` : `argent_${codeSuffix}_failed`,
|
|
1024
|
+
message: status === 'passed'
|
|
1025
|
+
? `Completed Argent driver action ${driverStep.driverAction}.`
|
|
1026
|
+
: `Argent driver action ${driverStep.driverAction} failed.`,
|
|
1027
|
+
metadata: {
|
|
1028
|
+
driverAction: driverStep.driverAction,
|
|
1029
|
+
...(failed
|
|
1030
|
+
? buildArgentFailureMetadata({
|
|
1031
|
+
driverAction: driverStep.driverAction,
|
|
1032
|
+
...(fallbackCapture?.capturePath ? { fallbackCapturePath: fallbackCapture.capturePath } : {}),
|
|
1033
|
+
missingRequiredScreenshot,
|
|
1034
|
+
rawFileName: driverResult.rawFileName,
|
|
1035
|
+
result: driverResult,
|
|
1036
|
+
rootOnlyDescription,
|
|
1037
|
+
})
|
|
1038
|
+
: {}),
|
|
1039
|
+
...buildArgentSelectorHealthMetadata(driverStep.selector),
|
|
1040
|
+
stepId: driverStep.stepId,
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
if (fallbackCapture) {
|
|
1044
|
+
checks.push({
|
|
1045
|
+
name: 'ios_simctl_screenshot_fallback',
|
|
1046
|
+
status: fallbackCapture.capturePath ? 'passed' : driverStep.required ? 'failed' : 'warning',
|
|
1047
|
+
source: 'runner',
|
|
1048
|
+
code: fallbackCapture.capturePath
|
|
1049
|
+
? 'ios_simctl_screenshot_fallback_completed'
|
|
1050
|
+
: 'ios_simctl_screenshot_fallback_failed',
|
|
1051
|
+
message: fallbackCapture.capturePath
|
|
1052
|
+
? 'Captured iOS screenshot through simctl after Argent screenshot was unavailable.'
|
|
1053
|
+
: 'iOS simctl screenshot fallback failed after Argent screenshot was unavailable.',
|
|
1054
|
+
metadata: {
|
|
1055
|
+
driverAction: driverStep.driverAction,
|
|
1056
|
+
provider: 'ios-simctl',
|
|
1057
|
+
rawPath: `raw/${fallbackCapture.rawFileName}`,
|
|
1058
|
+
...(fallbackCapture.capturePath ? { capturePath: fallbackCapture.capturePath } : {}),
|
|
1059
|
+
stepId: driverStep.stepId,
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
driverActionMetadata.push({
|
|
1064
|
+
args: driverResult.args,
|
|
1065
|
+
driverAction: driverStep.driverAction,
|
|
1066
|
+
exitCode: driverResult.exitCode,
|
|
1067
|
+
...(stableCapturePath ? { capturePath: stableCapturePath } : {}),
|
|
1068
|
+
...(fallbackCapture?.capturePath ? { captureProvider: 'ios-simctl' } : {}),
|
|
1069
|
+
rawPath: `raw/${driverResult.rawFileName}`,
|
|
1070
|
+
...(driverStep.selector ? { selector: driverStep.selector } : {}),
|
|
1071
|
+
stepId: driverStep.stepId,
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
metadata.driverActions = driverActionMetadata;
|
|
1075
|
+
metadata.captures = captures;
|
|
1076
|
+
const health = buildArgentHealth({
|
|
1077
|
+
checks,
|
|
1078
|
+
...(executionPlan.flowId ? { flowId: executionPlan.flowId } : {}),
|
|
1079
|
+
runId,
|
|
1080
|
+
scenarioId: executionPlan.scenarioId,
|
|
1081
|
+
});
|
|
1082
|
+
const verdict = buildArgentVerdict({
|
|
1083
|
+
...(executionPlan.flowId ? { flowId: executionPlan.flowId } : {}),
|
|
1084
|
+
health,
|
|
1085
|
+
runId,
|
|
1086
|
+
scenarioId: executionPlan.scenarioId,
|
|
1087
|
+
});
|
|
1088
|
+
const agentSummary = buildAgentSummaryMarkdown({ health, verdict });
|
|
1089
|
+
await Promise.all(Object.entries(raw).map(([fileName, content]) => fsp.writeFile(path.join(rawDir, fileName), `${content.trimEnd()}\n`, 'utf8')));
|
|
1090
|
+
await fsp.writeFile(path.join(rawDir, 'argent-metadata.json'), `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
|
|
1091
|
+
await writeJsonArtifact({
|
|
1092
|
+
filePath: layout.health,
|
|
1093
|
+
value: health,
|
|
1094
|
+
schema: SCHEMAS.health,
|
|
1095
|
+
label: 'Health artifact',
|
|
1096
|
+
});
|
|
1097
|
+
await writeJsonArtifact({
|
|
1098
|
+
filePath: layout.verdict,
|
|
1099
|
+
value: verdict,
|
|
1100
|
+
schema: SCHEMAS.verdict,
|
|
1101
|
+
label: 'Verdict artifact',
|
|
1102
|
+
});
|
|
1103
|
+
await writeTextArtifact({
|
|
1104
|
+
filePath: layout.agentSummary,
|
|
1105
|
+
content: agentSummary,
|
|
1106
|
+
});
|
|
1107
|
+
return {
|
|
1108
|
+
agentSummary,
|
|
1109
|
+
captures,
|
|
1110
|
+
health,
|
|
1111
|
+
metadata,
|
|
1112
|
+
raw,
|
|
1113
|
+
runDir,
|
|
1114
|
+
verdict,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Runs the Argent capture CLI.
|
|
1119
|
+
*
|
|
1120
|
+
* @returns {Promise<void>}
|
|
1121
|
+
*/
|
|
1122
|
+
async function main() {
|
|
1123
|
+
const argv = process.argv.slice(2);
|
|
1124
|
+
if (hasHelpFlag(argv)) {
|
|
1125
|
+
usage(process.stdout);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
loadAslLocalEnv();
|
|
1129
|
+
const args = parseArgs(argv);
|
|
1130
|
+
const argentCommand = readStringArgOrEnv(args.argent, ['ASL_ARGENT_BIN']);
|
|
1131
|
+
const baseArgs = parseBaseArgs(readStringArgOrEnv(args['base-args'], ['ASL_ARGENT_BASE_ARGS']));
|
|
1132
|
+
const commandTimeoutMs = readPositiveInteger(readStringArgOrEnv(args['command-timeout-ms'], ['ASL_ARGENT_COMMAND_TIMEOUT_MS']), 60_000);
|
|
1133
|
+
if (args.check === true || args.check === 'true') {
|
|
1134
|
+
const result = await checkArgentAvailability({
|
|
1135
|
+
...(argentCommand ? { argentCommand } : {}),
|
|
1136
|
+
...(baseArgs ? { baseArgs } : {}),
|
|
1137
|
+
commandTimeoutMs,
|
|
1138
|
+
});
|
|
1139
|
+
if (typeof args.out === 'string') {
|
|
1140
|
+
await writeArgentAvailabilityArtifacts({
|
|
1141
|
+
outputDir: args.out,
|
|
1142
|
+
result,
|
|
1143
|
+
...(typeof args['run-id'] === 'string' ? { runId: args['run-id'] } : {}),
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
1147
|
+
if (result.status !== 'passed') {
|
|
1148
|
+
process.exitCode = 1;
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (typeof args.platform !== 'string' || typeof args.scenario !== 'string') {
|
|
1153
|
+
usage();
|
|
1154
|
+
process.exitCode = 1;
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (!['android', 'ios'].includes(args.platform)) {
|
|
1158
|
+
throw new Error('--platform must be one of android or ios.');
|
|
1159
|
+
}
|
|
1160
|
+
const platform = args.platform;
|
|
1161
|
+
const app = readStringArgOrEnv(args.app, platform === 'ios'
|
|
1162
|
+
? ['ASL_IOS_APP_ID', 'ASL_EXAMPLE_IOS_APP_ID']
|
|
1163
|
+
: ['ASL_ANDROID_APP_ID', 'ASL_EXAMPLE_ANDROID_APP_ID']);
|
|
1164
|
+
const envDevice = readStringArgOrEnv(undefined, platform === 'ios'
|
|
1165
|
+
? ['ASL_IOS_UDID', 'ASL_EXAMPLE_IOS_UDID']
|
|
1166
|
+
: ['ASL_ANDROID_SERIAL', 'ASL_EXAMPLE_ANDROID_SERIAL']);
|
|
1167
|
+
const deviceFlag = readStringArgOrEnv(args['device-flag'], ['ASL_ARGENT_DEVICE_FLAG']);
|
|
1168
|
+
const appFlag = readStringArgOrEnv(args['app-flag'], ['ASL_ARGENT_APP_FLAG']);
|
|
1169
|
+
const xcrunPath = readStringArgOrEnv(args.xcrun, ['ASL_XCRUN_PATH', 'ASL_IOS_XCRUN_BIN']);
|
|
1170
|
+
const deviceId = typeof args.device === 'string'
|
|
1171
|
+
? args.device
|
|
1172
|
+
: platform === 'ios' && typeof args.udid === 'string'
|
|
1173
|
+
? args.udid
|
|
1174
|
+
: platform === 'android' && typeof args.serial === 'string'
|
|
1175
|
+
? args.serial
|
|
1176
|
+
: envDevice ?? null;
|
|
1177
|
+
if (!deviceId) {
|
|
1178
|
+
usage();
|
|
1179
|
+
process.exitCode = 1;
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const screenSize = readScreenSize({ height: args['screen-height'], width: args['screen-width'] });
|
|
1183
|
+
const result = await runArgentCapture({
|
|
1184
|
+
...(app ? { app } : {}),
|
|
1185
|
+
...(appFlag ? { appFlag } : {}),
|
|
1186
|
+
...(argentCommand ? { argentCommand } : {}),
|
|
1187
|
+
...(baseArgs ? { baseArgs } : {}),
|
|
1188
|
+
commandTimeoutMs,
|
|
1189
|
+
...(deviceFlag ? { deviceFlag } : {}),
|
|
1190
|
+
deviceId,
|
|
1191
|
+
iosSimctlScreenshotFallback: args['ios-simctl-screenshot-fallback'] === true ||
|
|
1192
|
+
readBooleanArgOrEnv(args['ios-simctl-screenshot-fallback'], ['ASL_ARGENT_IOS_SIMCTL_SCREENSHOT_FALLBACK']),
|
|
1193
|
+
...(typeof args.out === 'string' ? { outputDir: args.out } : {}),
|
|
1194
|
+
platform,
|
|
1195
|
+
...(typeof args['run-id'] === 'string' ? { runId: args['run-id'] } : {}),
|
|
1196
|
+
scenario: readJson(path.resolve(args.scenario)),
|
|
1197
|
+
...(screenSize ? { screenSize } : {}),
|
|
1198
|
+
waitMs: readPositiveInteger(args['wait-ms'], 0),
|
|
1199
|
+
...(xcrunPath ? { xcrunPath } : {}),
|
|
1200
|
+
});
|
|
1201
|
+
process.stdout.write(`${result.runDir}\n`);
|
|
1202
|
+
if (result.health.healthStatus !== 'passed') {
|
|
1203
|
+
process.exitCode = 1;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (require.main === module) {
|
|
1207
|
+
main().catch((error) => {
|
|
1208
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1209
|
+
process.exitCode = 1;
|
|
1210
|
+
});
|
|
1211
|
+
}
|