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,812 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildCompatibilityHealth = buildCompatibilityHealth;
|
|
4
|
+
exports.buildUnevaluatedVerdict = buildUnevaluatedVerdict;
|
|
5
|
+
exports.collectProvidedDriverActions = collectProvidedDriverActions;
|
|
6
|
+
exports.collectScenarioDriverActions = collectScenarioDriverActions;
|
|
7
|
+
exports.evaluateRunnerCompatibility = evaluateRunnerCompatibility;
|
|
8
|
+
exports.intersection = intersection;
|
|
9
|
+
exports.uniqueSorted = uniqueSorted;
|
|
10
|
+
exports.validateScenarioAdapterOptions = validateScenarioAdapterOptions;
|
|
11
|
+
/**
|
|
12
|
+
* Returns `value` when it is already an array; otherwise returns an empty array.
|
|
13
|
+
*
|
|
14
|
+
* @param {unknown} value
|
|
15
|
+
* @returns {unknown[]}
|
|
16
|
+
*/
|
|
17
|
+
function asArray(value) {
|
|
18
|
+
return Array.isArray(value) ? value : [];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Returns non-empty string values with duplicates removed and deterministic ordering.
|
|
22
|
+
*
|
|
23
|
+
* @param {unknown[]} values
|
|
24
|
+
* @returns {string[]}
|
|
25
|
+
*/
|
|
26
|
+
function uniqueSorted(values) {
|
|
27
|
+
const strings = values.filter((value) => typeof value === 'string' && value.length > 0);
|
|
28
|
+
return [...new Set(strings)].sort();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Finds the unique string values shared by two arrays.
|
|
32
|
+
*
|
|
33
|
+
* @param {unknown[]} left
|
|
34
|
+
* @param {unknown[]} right
|
|
35
|
+
* @returns {string[]}
|
|
36
|
+
*/
|
|
37
|
+
function intersection(left, right) {
|
|
38
|
+
const rightSet = new Set(asArray(right));
|
|
39
|
+
return uniqueSorted(asArray(left).filter((value) => rightSet.has(value)));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns required values that are absent from the available set.
|
|
43
|
+
*
|
|
44
|
+
* @param {unknown[]} available
|
|
45
|
+
* @param {unknown[]} required
|
|
46
|
+
* @returns {unknown[]}
|
|
47
|
+
*/
|
|
48
|
+
function includesAll(available, required) {
|
|
49
|
+
const availableSet = new Set(asArray(available));
|
|
50
|
+
return asArray(required).filter((item) => !availableSet.has(item));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Creates a structured planner issue.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} code
|
|
56
|
+
* @param {string} message
|
|
57
|
+
* @param {Record<string, unknown>} [metadata]
|
|
58
|
+
* @returns {Record<string, unknown>}
|
|
59
|
+
*/
|
|
60
|
+
function createIssue(code, message, metadata = {}) {
|
|
61
|
+
return {
|
|
62
|
+
code,
|
|
63
|
+
message,
|
|
64
|
+
...metadata,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns `value` when it is a plain object; otherwise returns an empty object.
|
|
69
|
+
*
|
|
70
|
+
* @param {unknown} value
|
|
71
|
+
* @returns {Record<string, unknown>}
|
|
72
|
+
*/
|
|
73
|
+
function asObject(value) {
|
|
74
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Returns true when a value is a finite number.
|
|
78
|
+
*
|
|
79
|
+
* @param {unknown} value
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
function isFiniteNumber(value) {
|
|
83
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns true when a value is a positive integer.
|
|
87
|
+
*
|
|
88
|
+
* @param {unknown} value
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
function isPositiveInteger(value) {
|
|
92
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolves the stable scenario step identifier used in planner errors.
|
|
96
|
+
*
|
|
97
|
+
* @param {Record<string, unknown>} step
|
|
98
|
+
* @param {number} index
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
function getScenarioStepId(step, index) {
|
|
102
|
+
return typeof step.id === 'string' && step.id.length > 0 ? step.id : `step-${index + 1}`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Returns true when a scenario step has a portable selector object.
|
|
106
|
+
*
|
|
107
|
+
* @param {Record<string, unknown>} step
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*/
|
|
110
|
+
function hasPortableSelector(step) {
|
|
111
|
+
const selector = asObject(step.selector);
|
|
112
|
+
return typeof selector.kind === 'string' && typeof selector.value === 'string' && selector.value.length > 0;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Returns true when a value can be used as an agent-device ref target.
|
|
116
|
+
*
|
|
117
|
+
* @param {unknown} value
|
|
118
|
+
* @returns {boolean}
|
|
119
|
+
*/
|
|
120
|
+
function hasAgentDeviceRef(value) {
|
|
121
|
+
return typeof value === 'string' && value.length > 0;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Returns true when a scenario step has an agent-device tap target.
|
|
125
|
+
*
|
|
126
|
+
* @param {Record<string, unknown>} step
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
*/
|
|
129
|
+
function hasAgentDeviceTapTarget(step) {
|
|
130
|
+
const agentDevice = asObject(asObject(step.adapterOptions).agentDevice);
|
|
131
|
+
return (hasPortableSelector(step) ||
|
|
132
|
+
hasAgentDeviceRef(agentDevice.ref) ||
|
|
133
|
+
(isFiniteNumber(agentDevice.x) && isFiniteNumber(agentDevice.y)));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Returns true when a scenario step has Argent tap coordinates.
|
|
137
|
+
*
|
|
138
|
+
* @param {Record<string, unknown>} step
|
|
139
|
+
* @returns {boolean}
|
|
140
|
+
*/
|
|
141
|
+
function hasArgentTapTarget(step) {
|
|
142
|
+
const argent = asObject(asObject(step.adapterOptions).argent);
|
|
143
|
+
return isFiniteNumber(argent.x) && isFiniteNumber(argent.y);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolves the stable scenario identifier used in generated artifacts.
|
|
147
|
+
*
|
|
148
|
+
* @param {Record<string, unknown> | null | undefined} scenario
|
|
149
|
+
* @returns {string}
|
|
150
|
+
*/
|
|
151
|
+
function getScenarioId(scenario) {
|
|
152
|
+
return scenario?.id ?? scenario?.name ?? 'unknown-scenario';
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolves the stable runner identifier used in planner errors.
|
|
156
|
+
*
|
|
157
|
+
* @param {Record<string, unknown> | null | undefined} runner
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
function getRunnerId(runner) {
|
|
161
|
+
return runner?.runnerId ?? runner?.name ?? 'unknown-runner';
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Keeps only scalar issue metadata so health artifacts remain schema-safe.
|
|
165
|
+
*
|
|
166
|
+
* @param {Record<string, unknown>} issue
|
|
167
|
+
* @returns {Record<string, string | number | boolean | null>}
|
|
168
|
+
*/
|
|
169
|
+
function getIssueMetadata(issue) {
|
|
170
|
+
return Object.keys(issue)
|
|
171
|
+
.filter((key) => key !== 'code' && key !== 'message')
|
|
172
|
+
.sort()
|
|
173
|
+
.reduce((metadata, key) => {
|
|
174
|
+
const value = issue[key];
|
|
175
|
+
if (typeof value === 'string' ||
|
|
176
|
+
typeof value === 'number' ||
|
|
177
|
+
typeof value === 'boolean' ||
|
|
178
|
+
value === null) {
|
|
179
|
+
metadata[key] = value;
|
|
180
|
+
}
|
|
181
|
+
return metadata;
|
|
182
|
+
}, {});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Converts a planner issue into a `health.json` check entry.
|
|
186
|
+
*
|
|
187
|
+
* @param {Record<string, unknown>} issue
|
|
188
|
+
* @param {'failed' | 'warning'} status
|
|
189
|
+
* @returns {Record<string, unknown>}
|
|
190
|
+
*/
|
|
191
|
+
function issueToHealthCheck(issue, status) {
|
|
192
|
+
const metadata = getIssueMetadata(issue);
|
|
193
|
+
return {
|
|
194
|
+
name: issue.code,
|
|
195
|
+
status,
|
|
196
|
+
source: issue.code?.includes('artifact') || issue.code?.includes('evidence')
|
|
197
|
+
? 'evidence'
|
|
198
|
+
: 'planner',
|
|
199
|
+
code: issue.code,
|
|
200
|
+
message: issue.message,
|
|
201
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Returns whether a provider can participate in the selected platform set.
|
|
206
|
+
*
|
|
207
|
+
* @param {Record<string, unknown>} provider
|
|
208
|
+
* @param {string[]} effectivePlatforms
|
|
209
|
+
* @returns {boolean}
|
|
210
|
+
*/
|
|
211
|
+
function isProviderActiveForPlatforms(provider, effectivePlatforms) {
|
|
212
|
+
return intersection(asArray(provider?.platforms), effectivePlatforms).length > 0;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Collects artifacts available from the primary runner and active evidence providers.
|
|
216
|
+
*
|
|
217
|
+
* @param {{runner: Record<string, unknown>, evidenceProviders: Record<string, unknown>[], effectivePlatforms: string[]}} options
|
|
218
|
+
* @returns {{activeProviders: Record<string, unknown>[], artifacts: string[]}}
|
|
219
|
+
*/
|
|
220
|
+
function collectProvidedArtifacts({ runner, evidenceProviders, effectivePlatforms, }) {
|
|
221
|
+
const activeProviders = evidenceProviders.filter((provider) => isProviderActiveForPlatforms(provider, effectivePlatforms));
|
|
222
|
+
return {
|
|
223
|
+
activeProviders,
|
|
224
|
+
artifacts: uniqueSorted([
|
|
225
|
+
...asArray(runner?.artifactOutputs),
|
|
226
|
+
...activeProviders.flatMap((provider) => asArray(provider?.artifactOutputs)),
|
|
227
|
+
]),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Collects capabilities available from the primary runner and active evidence providers.
|
|
232
|
+
*
|
|
233
|
+
* @param {{runner: Record<string, unknown>, evidenceProviders: Record<string, unknown>[], effectivePlatforms: string[]}} options
|
|
234
|
+
* @returns {string[]}
|
|
235
|
+
*/
|
|
236
|
+
function collectProvidedCapabilities({ runner, evidenceProviders, effectivePlatforms, }) {
|
|
237
|
+
const activeProviders = evidenceProviders.filter((provider) => isProviderActiveForPlatforms(provider, effectivePlatforms));
|
|
238
|
+
return uniqueSorted([
|
|
239
|
+
...asArray(runner?.capabilities),
|
|
240
|
+
...activeProviders.flatMap((provider) => asArray(provider?.capabilities)),
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Collects driver operations available from the primary runner and active providers.
|
|
245
|
+
*
|
|
246
|
+
* @param {{runner: Record<string, unknown>, evidenceProviders: Record<string, unknown>[], effectivePlatforms: string[]}} options
|
|
247
|
+
* @returns {string[]}
|
|
248
|
+
*/
|
|
249
|
+
function collectProvidedDriverActions({ runner, evidenceProviders, effectivePlatforms, }) {
|
|
250
|
+
const activeProviders = evidenceProviders.filter((provider) => isProviderActiveForPlatforms(provider, effectivePlatforms));
|
|
251
|
+
return uniqueSorted([
|
|
252
|
+
...asArray(runner?.driverActions),
|
|
253
|
+
...activeProviders.flatMap((provider) => asArray(provider?.driverActions)),
|
|
254
|
+
]);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Collects driver operations required by scenario steps.
|
|
258
|
+
*
|
|
259
|
+
* @param {Record<string, unknown>} scenario
|
|
260
|
+
* @returns {{required: string[], optional: string[]}}
|
|
261
|
+
*/
|
|
262
|
+
function collectScenarioDriverActions(scenario) {
|
|
263
|
+
const steps = Array.isArray(scenario.steps) ? scenario.steps : [];
|
|
264
|
+
const required = [];
|
|
265
|
+
const optional = [];
|
|
266
|
+
for (const step of steps) {
|
|
267
|
+
if (!step || typeof step !== 'object' || typeof step.driverAction !== 'string') {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (step.required === false) {
|
|
271
|
+
optional.push(step.driverAction);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
required.push(step.driverAction);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
required: uniqueSorted(required),
|
|
279
|
+
optional: uniqueSorted(optional),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Adds a structured invalid-adapter-options issue.
|
|
284
|
+
*
|
|
285
|
+
* @param {{adapter: string, errors: PlannerIssue[], field: string, message: string, scenario: ScenarioManifest, stepId?: string}} options
|
|
286
|
+
* @returns {void}
|
|
287
|
+
*/
|
|
288
|
+
function pushInvalidAdapterOption({ adapter, errors, field, message, scenario, stepId, }) {
|
|
289
|
+
errors.push(createIssue('invalid_adapter_options', message, {
|
|
290
|
+
adapter,
|
|
291
|
+
field,
|
|
292
|
+
scenarioId: getScenarioId(scenario),
|
|
293
|
+
...(stepId ? { stepId } : {}),
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Validates Android adb metadata that built-in runners depend on at runtime.
|
|
298
|
+
*
|
|
299
|
+
* @param {{scenario: ScenarioManifest, errors: PlannerIssue[]}} options
|
|
300
|
+
* @returns {void}
|
|
301
|
+
*/
|
|
302
|
+
function validateAndroidAdbAdapterOptions({ errors, scenario, }) {
|
|
303
|
+
const steps = Array.isArray(scenario.steps) ? scenario.steps : [];
|
|
304
|
+
for (const [index, step] of steps.entries()) {
|
|
305
|
+
if (!step || typeof step !== 'object') {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const androidAdb = asObject(asObject(step.adapterOptions).androidAdb);
|
|
309
|
+
const stepId = getScenarioStepId(step, index);
|
|
310
|
+
if (step.driverAction === 'assertVisible' &&
|
|
311
|
+
!hasPortableSelector(step)) {
|
|
312
|
+
pushInvalidAdapterOption({
|
|
313
|
+
adapter: 'androidAdb',
|
|
314
|
+
errors,
|
|
315
|
+
field: 'selector',
|
|
316
|
+
message: `Step \`${stepId}\` uses driverAction \`assertVisible\` but a portable selector is required.`,
|
|
317
|
+
scenario,
|
|
318
|
+
stepId,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (step.driverAction === 'tap' &&
|
|
322
|
+
!hasPortableSelector(step) &&
|
|
323
|
+
(!isFiniteNumber(androidAdb.x) || !isFiniteNumber(androidAdb.y))) {
|
|
324
|
+
pushInvalidAdapterOption({
|
|
325
|
+
adapter: 'androidAdb',
|
|
326
|
+
errors,
|
|
327
|
+
field: 'x/y',
|
|
328
|
+
message: `Step \`${stepId}\` uses driverAction \`tap\` but adapterOptions.androidAdb.x/y are required.`,
|
|
329
|
+
scenario,
|
|
330
|
+
stepId,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (step.driverAction === 'scroll' &&
|
|
334
|
+
!hasPortableSelector(step) &&
|
|
335
|
+
(!isFiniteNumber(androidAdb.startX) ||
|
|
336
|
+
!isFiniteNumber(androidAdb.startY) ||
|
|
337
|
+
!isFiniteNumber(androidAdb.endX) ||
|
|
338
|
+
!isFiniteNumber(androidAdb.endY))) {
|
|
339
|
+
pushInvalidAdapterOption({
|
|
340
|
+
adapter: 'androidAdb',
|
|
341
|
+
errors,
|
|
342
|
+
field: 'startX/startY/endX/endY',
|
|
343
|
+
message: `Step \`${stepId}\` uses driverAction \`scroll\` but adapterOptions.androidAdb.startX/startY/endX/endY are required.`,
|
|
344
|
+
scenario,
|
|
345
|
+
stepId,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if ('durationMs' in androidAdb && !isPositiveInteger(androidAdb.durationMs)) {
|
|
349
|
+
pushInvalidAdapterOption({
|
|
350
|
+
adapter: 'androidAdb',
|
|
351
|
+
errors,
|
|
352
|
+
field: 'durationMs',
|
|
353
|
+
message: `Step \`${stepId}\` has adapterOptions.androidAdb.durationMs, but it must be a positive integer.`,
|
|
354
|
+
scenario,
|
|
355
|
+
stepId,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
if ('logcatLines' in androidAdb && !isPositiveInteger(androidAdb.logcatLines)) {
|
|
359
|
+
pushInvalidAdapterOption({
|
|
360
|
+
adapter: 'androidAdb',
|
|
361
|
+
errors,
|
|
362
|
+
field: 'logcatLines',
|
|
363
|
+
message: `Step \`${stepId}\` has adapterOptions.androidAdb.logcatLines, but it must be a positive integer.`,
|
|
364
|
+
scenario,
|
|
365
|
+
stepId,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
if ('waitMs' in androidAdb && !isPositiveInteger(androidAdb.waitMs)) {
|
|
369
|
+
pushInvalidAdapterOption({
|
|
370
|
+
adapter: 'androidAdb',
|
|
371
|
+
errors,
|
|
372
|
+
field: 'waitMs',
|
|
373
|
+
message: `Step \`${stepId}\` has adapterOptions.androidAdb.waitMs, but it must be a positive integer.`,
|
|
374
|
+
scenario,
|
|
375
|
+
stepId,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Validates iOS simctl metadata that built-in runners depend on at runtime.
|
|
382
|
+
*
|
|
383
|
+
* @param {{scenario: ScenarioManifest, errors: PlannerIssue[]}} options
|
|
384
|
+
* @returns {void}
|
|
385
|
+
*/
|
|
386
|
+
function validateIosSimctlAdapterOptions({ errors, scenario, }) {
|
|
387
|
+
const scenarioIosSimctl = asObject(asObject(scenario.adapterOptions).iosSimctl);
|
|
388
|
+
if ('repeat' in scenarioIosSimctl && !isPositiveInteger(scenarioIosSimctl.repeat)) {
|
|
389
|
+
pushInvalidAdapterOption({
|
|
390
|
+
adapter: 'iosSimctl',
|
|
391
|
+
errors,
|
|
392
|
+
field: 'repeat',
|
|
393
|
+
message: 'Scenario adapterOptions.iosSimctl.repeat must be a positive integer.',
|
|
394
|
+
scenario,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if ('commands' in scenarioIosSimctl) {
|
|
398
|
+
if (!Array.isArray(scenarioIosSimctl.commands)) {
|
|
399
|
+
pushInvalidAdapterOption({
|
|
400
|
+
adapter: 'iosSimctl',
|
|
401
|
+
errors,
|
|
402
|
+
field: 'commands',
|
|
403
|
+
message: 'Scenario adapterOptions.iosSimctl.commands must be an array when provided.',
|
|
404
|
+
scenario,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
for (const [index, command] of scenarioIosSimctl.commands.entries()) {
|
|
409
|
+
const commandOptions = asObject(command);
|
|
410
|
+
if (typeof commandOptions.command !== 'string' || commandOptions.command.length === 0) {
|
|
411
|
+
pushInvalidAdapterOption({
|
|
412
|
+
adapter: 'iosSimctl',
|
|
413
|
+
errors,
|
|
414
|
+
field: `commands[${index}].command`,
|
|
415
|
+
message: `Scenario adapterOptions.iosSimctl.commands[${index}].command must be a non-empty string.`,
|
|
416
|
+
scenario,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
if ('waitMs' in commandOptions && !isPositiveInteger(commandOptions.waitMs)) {
|
|
420
|
+
pushInvalidAdapterOption({
|
|
421
|
+
adapter: 'iosSimctl',
|
|
422
|
+
errors,
|
|
423
|
+
field: `commands[${index}].waitMs`,
|
|
424
|
+
message: `Scenario adapterOptions.iosSimctl.commands[${index}].waitMs must be a positive integer.`,
|
|
425
|
+
scenario,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const steps = Array.isArray(scenario.steps) ? scenario.steps : [];
|
|
432
|
+
for (const [index, step] of steps.entries()) {
|
|
433
|
+
if (!step || typeof step !== 'object') {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const iosSimctl = asObject(asObject(step.adapterOptions).iosSimctl);
|
|
437
|
+
if ('waitMs' in iosSimctl && !isPositiveInteger(iosSimctl.waitMs)) {
|
|
438
|
+
const stepId = getScenarioStepId(step, index);
|
|
439
|
+
pushInvalidAdapterOption({
|
|
440
|
+
adapter: 'iosSimctl',
|
|
441
|
+
errors,
|
|
442
|
+
field: 'waitMs',
|
|
443
|
+
message: `Step \`${stepId}\` has adapterOptions.iosSimctl.waitMs, but it must be a positive integer.`,
|
|
444
|
+
scenario,
|
|
445
|
+
stepId,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Validates agent-device metadata that the bundled adapter enforces at runtime.
|
|
452
|
+
*
|
|
453
|
+
* @param {{scenario: ScenarioManifest, errors: PlannerIssue[]}} options
|
|
454
|
+
* @returns {void}
|
|
455
|
+
*/
|
|
456
|
+
function validateAgentDeviceAdapterOptions({ errors, scenario, }) {
|
|
457
|
+
const steps = Array.isArray(scenario.steps) ? scenario.steps : [];
|
|
458
|
+
for (const [index, step] of steps.entries()) {
|
|
459
|
+
if (!step || typeof step !== 'object') {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const selector = asObject(step.selector);
|
|
463
|
+
const stepId = getScenarioStepId(step, index);
|
|
464
|
+
if (typeof selector.match === 'string' &&
|
|
465
|
+
selector.match !== 'exact' &&
|
|
466
|
+
['assertVisible', 'tap'].includes(String(step.driverAction))) {
|
|
467
|
+
pushInvalidAdapterOption({
|
|
468
|
+
adapter: 'agentDevice',
|
|
469
|
+
errors,
|
|
470
|
+
field: 'selector.match',
|
|
471
|
+
message: `Step \`${stepId}\` uses selector match \`${selector.match}\`, but the agent-device adapter currently supports exact selector matches only.`,
|
|
472
|
+
scenario,
|
|
473
|
+
stepId,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (step.driverAction === 'assertVisible' && !hasPortableSelector(step)) {
|
|
477
|
+
pushInvalidAdapterOption({
|
|
478
|
+
adapter: 'agentDevice',
|
|
479
|
+
errors,
|
|
480
|
+
field: 'selector',
|
|
481
|
+
message: `Step \`${stepId}\` uses driverAction \`assertVisible\` but a portable selector is required.`,
|
|
482
|
+
scenario,
|
|
483
|
+
stepId,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
if (step.driverAction === 'tap' && !hasAgentDeviceTapTarget(step)) {
|
|
487
|
+
pushInvalidAdapterOption({
|
|
488
|
+
adapter: 'agentDevice',
|
|
489
|
+
errors,
|
|
490
|
+
field: 'selector/ref/x/y',
|
|
491
|
+
message: `Step \`${stepId}\` uses driverAction \`tap\` but requires a selector, adapterOptions.agentDevice.ref, or adapterOptions.agentDevice.x/y.`,
|
|
492
|
+
scenario,
|
|
493
|
+
stepId,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Validates Argent metadata that the optional adapter enforces at runtime.
|
|
500
|
+
*
|
|
501
|
+
* @param {{scenario: ScenarioManifest, errors: PlannerIssue[]}} options
|
|
502
|
+
* @returns {void}
|
|
503
|
+
*/
|
|
504
|
+
function validateArgentAdapterOptions({ errors, scenario, }) {
|
|
505
|
+
const steps = Array.isArray(scenario.steps) ? scenario.steps : [];
|
|
506
|
+
for (const [index, step] of steps.entries()) {
|
|
507
|
+
if (!step || typeof step !== 'object') {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const argent = asObject(asObject(step.adapterOptions).argent);
|
|
511
|
+
const selector = asObject(step.selector);
|
|
512
|
+
const stepId = getScenarioStepId(step, index);
|
|
513
|
+
if (step.driverAction === 'assertVisible' && !hasPortableSelector(step)) {
|
|
514
|
+
pushInvalidAdapterOption({
|
|
515
|
+
adapter: 'argent',
|
|
516
|
+
errors,
|
|
517
|
+
field: 'selector',
|
|
518
|
+
message: `Step \`${stepId}\` uses driverAction \`assertVisible\` but a portable selector is required.`,
|
|
519
|
+
scenario,
|
|
520
|
+
stepId,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
if (step.driverAction === 'tap' && !hasArgentTapTarget(step)) {
|
|
524
|
+
pushInvalidAdapterOption({
|
|
525
|
+
adapter: 'argent',
|
|
526
|
+
errors,
|
|
527
|
+
field: 'x/y',
|
|
528
|
+
message: `Step \`${stepId}\` uses driverAction \`tap\` but adapterOptions.argent.x/y are required.`,
|
|
529
|
+
scenario,
|
|
530
|
+
stepId,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (step.driverAction === 'scroll' &&
|
|
534
|
+
(!isFiniteNumber(argent.startX) ||
|
|
535
|
+
!isFiniteNumber(argent.startY) ||
|
|
536
|
+
!isFiniteNumber(argent.endX) ||
|
|
537
|
+
!isFiniteNumber(argent.endY))) {
|
|
538
|
+
pushInvalidAdapterOption({
|
|
539
|
+
adapter: 'argent',
|
|
540
|
+
errors,
|
|
541
|
+
field: 'startX/startY/endX/endY',
|
|
542
|
+
message: `Step \`${stepId}\` uses driverAction \`scroll\` but adapterOptions.argent.startX/startY/endX/endY are required.`,
|
|
543
|
+
scenario,
|
|
544
|
+
stepId,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
if ('durationMs' in argent && !isPositiveInteger(argent.durationMs)) {
|
|
548
|
+
pushInvalidAdapterOption({
|
|
549
|
+
adapter: 'argent',
|
|
550
|
+
errors,
|
|
551
|
+
field: 'durationMs',
|
|
552
|
+
message: `Step \`${stepId}\` has adapterOptions.argent.durationMs, but it must be a positive integer.`,
|
|
553
|
+
scenario,
|
|
554
|
+
stepId,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Validates adapter-specific scenario metadata for the selected platform set.
|
|
561
|
+
*
|
|
562
|
+
* @param {{scenario: ScenarioManifest, effectivePlatforms: string[], errors: PlannerIssue[], runner?: RunnerManifest}} options
|
|
563
|
+
* @returns {void}
|
|
564
|
+
*/
|
|
565
|
+
function validateScenarioAdapterOptions({ effectivePlatforms, errors, runner, scenario, }) {
|
|
566
|
+
const runnerId = runner ? getRunnerId(runner) : '';
|
|
567
|
+
const usesAndroidAdbOptions = Array.isArray(scenario.steps) &&
|
|
568
|
+
scenario.steps.some((step) => Object.prototype.hasOwnProperty.call(asObject(asObject(step).adapterOptions), 'androidAdb'));
|
|
569
|
+
if (effectivePlatforms.includes('android') && (runnerId.includes('adb') || usesAndroidAdbOptions)) {
|
|
570
|
+
validateAndroidAdbAdapterOptions({ errors, scenario });
|
|
571
|
+
}
|
|
572
|
+
if (effectivePlatforms.includes('ios')) {
|
|
573
|
+
validateIosSimctlAdapterOptions({ errors, scenario });
|
|
574
|
+
}
|
|
575
|
+
if (runnerId.includes('agent-device')) {
|
|
576
|
+
validateAgentDeviceAdapterOptions({ errors, scenario });
|
|
577
|
+
}
|
|
578
|
+
const usesArgentOptions = Array.isArray(scenario.steps) &&
|
|
579
|
+
scenario.steps.some((step) => Object.prototype.hasOwnProperty.call(asObject(asObject(step).adapterOptions), 'argent'));
|
|
580
|
+
if (runnerId.includes('argent') || usesArgentOptions) {
|
|
581
|
+
validateArgentAdapterOptions({ errors, scenario });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Adds planner errors when the selected runner cannot own a run lifecycle.
|
|
586
|
+
*
|
|
587
|
+
* @param {{runner: Record<string, unknown>, errors: Record<string, unknown>[]}} options
|
|
588
|
+
* @returns {void}
|
|
589
|
+
*/
|
|
590
|
+
function validatePrimaryRunner({ runner, errors, }) {
|
|
591
|
+
if (!runner || typeof runner !== 'object') {
|
|
592
|
+
errors.push(createIssue('runner_missing', 'A primary runner capability manifest is required.'));
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (runner.kind && runner.kind !== 'primary') {
|
|
596
|
+
errors.push(createIssue('runner_not_primary', 'The selected runner must have kind `primary`.', {
|
|
597
|
+
runnerId: getRunnerId(runner),
|
|
598
|
+
kind: runner.kind,
|
|
599
|
+
}));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Determines the platform set a plan should evaluate and records platform mismatches.
|
|
604
|
+
*
|
|
605
|
+
* @param {{scenario: Record<string, unknown>, runner: Record<string, unknown>, platform?: string | null, errors: Record<string, unknown>[]}} options
|
|
606
|
+
* @returns {string[]}
|
|
607
|
+
*/
|
|
608
|
+
function resolveEffectivePlatforms({ scenario, runner, platform, errors, }) {
|
|
609
|
+
const scenarioPlatforms = asArray(scenario?.platforms);
|
|
610
|
+
const runnerPlatforms = asArray(runner?.platforms);
|
|
611
|
+
if (platform) {
|
|
612
|
+
if (!scenarioPlatforms.includes(platform)) {
|
|
613
|
+
errors.push(createIssue('platform_not_supported_by_scenario', 'The scenario does not support the selected platform.', {
|
|
614
|
+
scenarioId: getScenarioId(scenario),
|
|
615
|
+
platform,
|
|
616
|
+
supportedPlatforms: uniqueSorted(scenarioPlatforms),
|
|
617
|
+
}));
|
|
618
|
+
}
|
|
619
|
+
if (!runnerPlatforms.includes(platform)) {
|
|
620
|
+
errors.push(createIssue('platform_not_supported_by_runner', 'The runner does not support the selected platform.', {
|
|
621
|
+
runnerId: getRunnerId(runner),
|
|
622
|
+
platform,
|
|
623
|
+
supportedPlatforms: uniqueSorted(runnerPlatforms),
|
|
624
|
+
}));
|
|
625
|
+
}
|
|
626
|
+
return [platform];
|
|
627
|
+
}
|
|
628
|
+
const commonPlatforms = intersection(scenarioPlatforms, runnerPlatforms);
|
|
629
|
+
if (commonPlatforms.length === 0) {
|
|
630
|
+
errors.push(createIssue('platform_mismatch', 'The scenario and runner do not share a supported platform.', {
|
|
631
|
+
scenarioId: getScenarioId(scenario),
|
|
632
|
+
runnerId: getRunnerId(runner),
|
|
633
|
+
scenarioPlatforms: uniqueSorted(scenarioPlatforms),
|
|
634
|
+
runnerPlatforms: uniqueSorted(runnerPlatforms),
|
|
635
|
+
}));
|
|
636
|
+
}
|
|
637
|
+
return commonPlatforms;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Evaluates whether a scenario can be served by a primary runner plus evidence providers.
|
|
641
|
+
*
|
|
642
|
+
* @param {{scenario?: Record<string, unknown>, runner?: Record<string, unknown>, evidenceProviders?: Record<string, unknown>[], platform?: string | null}} [options]
|
|
643
|
+
* @returns {{compatible: boolean, errors: Record<string, unknown>[], warnings: Record<string, unknown>[], matched: {platforms: string[], capabilities: string[], driverActions: string[], artifacts: string[], evidenceProviders: string[]}}}
|
|
644
|
+
*/
|
|
645
|
+
function evaluateRunnerCompatibility({ scenario, runner, evidenceProviders = [], platform = null, } = {}) {
|
|
646
|
+
const errors = [];
|
|
647
|
+
const warnings = [];
|
|
648
|
+
validatePrimaryRunner({ runner, errors });
|
|
649
|
+
const primaryRunner = runner ?? {};
|
|
650
|
+
if (!scenario || typeof scenario !== 'object') {
|
|
651
|
+
errors.push(createIssue('scenario_missing', 'A scenario manifest is required.'));
|
|
652
|
+
return {
|
|
653
|
+
compatible: false,
|
|
654
|
+
errors,
|
|
655
|
+
warnings,
|
|
656
|
+
matched: {
|
|
657
|
+
platforms: [],
|
|
658
|
+
capabilities: [],
|
|
659
|
+
driverActions: [],
|
|
660
|
+
artifacts: [],
|
|
661
|
+
evidenceProviders: [],
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
const effectivePlatforms = resolveEffectivePlatforms({ scenario, runner: primaryRunner, platform, errors });
|
|
666
|
+
validateScenarioAdapterOptions({ effectivePlatforms, errors, runner: primaryRunner, scenario });
|
|
667
|
+
const runnerCapabilities = uniqueSorted(asArray(primaryRunner.capabilities));
|
|
668
|
+
const missingRequiredCapabilities = includesAll(runnerCapabilities, asArray(scenario.requiredCapabilities));
|
|
669
|
+
for (const capability of missingRequiredCapabilities) {
|
|
670
|
+
errors.push(createIssue('missing_required_capability', `Runner \`${getRunnerId(primaryRunner)}\` is missing required capability \`${capability}\`.`, {
|
|
671
|
+
runnerId: getRunnerId(primaryRunner),
|
|
672
|
+
scenarioId: getScenarioId(scenario),
|
|
673
|
+
capability,
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
676
|
+
const providedCapabilities = collectProvidedCapabilities({
|
|
677
|
+
runner: primaryRunner,
|
|
678
|
+
evidenceProviders,
|
|
679
|
+
effectivePlatforms,
|
|
680
|
+
});
|
|
681
|
+
const missingOptionalCapabilities = includesAll(providedCapabilities, asArray(scenario.optionalCapabilities));
|
|
682
|
+
for (const capability of missingOptionalCapabilities) {
|
|
683
|
+
warnings.push(createIssue('missing_optional_capability', `No active runner or provider declares optional capability \`${capability}\`.`, {
|
|
684
|
+
scenarioId: getScenarioId(scenario),
|
|
685
|
+
capability,
|
|
686
|
+
}));
|
|
687
|
+
}
|
|
688
|
+
const providedDriverActions = collectProvidedDriverActions({
|
|
689
|
+
runner: primaryRunner,
|
|
690
|
+
evidenceProviders,
|
|
691
|
+
effectivePlatforms,
|
|
692
|
+
});
|
|
693
|
+
const scenarioDriverActions = collectScenarioDriverActions(scenario);
|
|
694
|
+
for (const driverAction of includesAll(providedDriverActions, scenarioDriverActions.required)) {
|
|
695
|
+
errors.push(createIssue('missing_required_driver_action', `No active runner or provider declares required driver action \`${driverAction}\`.`, {
|
|
696
|
+
runnerId: getRunnerId(primaryRunner),
|
|
697
|
+
scenarioId: getScenarioId(scenario),
|
|
698
|
+
driverAction,
|
|
699
|
+
}));
|
|
700
|
+
}
|
|
701
|
+
for (const driverAction of includesAll(providedDriverActions, scenarioDriverActions.optional)) {
|
|
702
|
+
warnings.push(createIssue('missing_optional_driver_action', `No active runner or provider declares optional driver action \`${driverAction}\`.`, {
|
|
703
|
+
runnerId: getRunnerId(primaryRunner),
|
|
704
|
+
scenarioId: getScenarioId(scenario),
|
|
705
|
+
driverAction,
|
|
706
|
+
}));
|
|
707
|
+
}
|
|
708
|
+
const { activeProviders, artifacts } = collectProvidedArtifacts({
|
|
709
|
+
runner: primaryRunner,
|
|
710
|
+
evidenceProviders,
|
|
711
|
+
effectivePlatforms,
|
|
712
|
+
});
|
|
713
|
+
const requiredArtifacts = asArray(scenario.artifacts?.required);
|
|
714
|
+
const optionalArtifacts = asArray(scenario.artifacts?.optional);
|
|
715
|
+
for (const artifact of includesAll(artifacts, requiredArtifacts)) {
|
|
716
|
+
errors.push(createIssue('missing_required_artifact', `No active runner or evidence provider can produce required artifact \`${artifact}\`.`, {
|
|
717
|
+
scenarioId: getScenarioId(scenario),
|
|
718
|
+
artifact,
|
|
719
|
+
}));
|
|
720
|
+
}
|
|
721
|
+
for (const artifact of includesAll(artifacts, optionalArtifacts)) {
|
|
722
|
+
warnings.push(createIssue('missing_optional_artifact', `No active runner or evidence provider declares optional artifact \`${artifact}\`.`, {
|
|
723
|
+
scenarioId: getScenarioId(scenario),
|
|
724
|
+
artifact,
|
|
725
|
+
}));
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
compatible: errors.length === 0,
|
|
729
|
+
errors,
|
|
730
|
+
warnings,
|
|
731
|
+
matched: {
|
|
732
|
+
platforms: effectivePlatforms,
|
|
733
|
+
capabilities: providedCapabilities,
|
|
734
|
+
driverActions: providedDriverActions,
|
|
735
|
+
artifacts,
|
|
736
|
+
evidenceProviders: activeProviders.map((provider) => getRunnerId(provider)),
|
|
737
|
+
},
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Builds the initial `health.json` artifact from planner compatibility results.
|
|
742
|
+
*
|
|
743
|
+
* @param {{scenario: Record<string, unknown>, runId?: string, compatibility: Record<string, unknown>}} options
|
|
744
|
+
* @returns {Record<string, unknown>}
|
|
745
|
+
*/
|
|
746
|
+
function buildCompatibilityHealth({ scenario, runId, compatibility, }) {
|
|
747
|
+
const scenarioId = getScenarioId(scenario);
|
|
748
|
+
const resolvedRunId = typeof runId === 'string' && runId.length > 0 ? runId : 'unknown-run';
|
|
749
|
+
const errors = compatibility.errors;
|
|
750
|
+
const warnings = compatibility.warnings;
|
|
751
|
+
const failedChecks = errors.map((issue) => issueToHealthCheck(issue, 'failed'));
|
|
752
|
+
const warningChecks = warnings.map((issue) => issueToHealthCheck(issue, 'warning'));
|
|
753
|
+
const checks = failedChecks.length > 0
|
|
754
|
+
? failedChecks
|
|
755
|
+
: [
|
|
756
|
+
{
|
|
757
|
+
name: 'planner_compatibility',
|
|
758
|
+
status: 'passed',
|
|
759
|
+
source: 'planner',
|
|
760
|
+
code: 'planner_compatible',
|
|
761
|
+
message: 'Scenario requirements are compatible with the selected runner and evidence providers.',
|
|
762
|
+
},
|
|
763
|
+
];
|
|
764
|
+
return {
|
|
765
|
+
schemaVersion: '1.0.0',
|
|
766
|
+
scenarioId,
|
|
767
|
+
...(typeof scenario?.flowId === 'string' ? { flowId: scenario.flowId } : {}),
|
|
768
|
+
runId: resolvedRunId,
|
|
769
|
+
healthStatus: failedChecks.length > 0 ? 'failed' : 'passed',
|
|
770
|
+
checks,
|
|
771
|
+
...(warningChecks.length > 0 ? { warnings: warningChecks } : {}),
|
|
772
|
+
matched: {
|
|
773
|
+
platforms: uniqueSorted(asArray(compatibility?.matched?.platforms)),
|
|
774
|
+
capabilities: uniqueSorted(asArray(compatibility?.matched?.capabilities)),
|
|
775
|
+
driverActions: uniqueSorted(asArray(compatibility?.matched?.driverActions)),
|
|
776
|
+
artifacts: uniqueSorted(asArray(compatibility?.matched?.artifacts)),
|
|
777
|
+
evidenceProviders: uniqueSorted(asArray(compatibility?.matched?.evidenceProviders)),
|
|
778
|
+
},
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Builds a pre-budget `verdict.json` artifact from scenario health.
|
|
783
|
+
*
|
|
784
|
+
* @param {{scenario: Record<string, unknown>, runId?: string, health: Record<string, unknown>}} options
|
|
785
|
+
* @returns {Record<string, unknown>}
|
|
786
|
+
*/
|
|
787
|
+
function buildUnevaluatedVerdict({ scenario, runId, health, }) {
|
|
788
|
+
const healthStatus = health?.healthStatus ?? 'failed';
|
|
789
|
+
const scenarioId = health?.scenarioId ?? getScenarioId(scenario);
|
|
790
|
+
const resolvedRunId = typeof health?.runId === 'string' && health.runId.length > 0
|
|
791
|
+
? health.runId
|
|
792
|
+
: typeof runId === 'string' && runId.length > 0
|
|
793
|
+
? runId
|
|
794
|
+
: 'unknown-run';
|
|
795
|
+
const canEvaluateBudgets = healthStatus === 'passed';
|
|
796
|
+
return {
|
|
797
|
+
schemaVersion: '1.0.0',
|
|
798
|
+
scenarioId,
|
|
799
|
+
...(typeof health?.flowId === 'string'
|
|
800
|
+
? { flowId: health.flowId }
|
|
801
|
+
: typeof scenario?.flowId === 'string'
|
|
802
|
+
? { flowId: scenario.flowId }
|
|
803
|
+
: {}),
|
|
804
|
+
runId: resolvedRunId,
|
|
805
|
+
healthStatus,
|
|
806
|
+
verdictStatus: canEvaluateBudgets ? 'not_evaluated' : 'inconclusive',
|
|
807
|
+
budgetChecks: [],
|
|
808
|
+
summary: canEvaluateBudgets
|
|
809
|
+
? 'Scenario health passed; budget evaluation has not run yet.'
|
|
810
|
+
: 'Scenario health did not pass; do not optimize from this run.',
|
|
811
|
+
};
|
|
812
|
+
}
|