@exodus/xqa 1.14.0 → 1.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -16
- package/dist/xqa.cjs +110 -69
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -14,12 +14,20 @@ The tool manages configuration, project initialization, session state tracking,
|
|
|
14
14
|
|
|
15
15
|
Initialize a new xqa project in the current directory.
|
|
16
16
|
|
|
17
|
-
Creates a `.xqa/` directory with
|
|
17
|
+
Creates a `.xqa/` directory with `app.md` and `explore.md` templates plus subdirectories for specs, designs, and suites. Installs bundled xqa skills.
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
xqa init
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
### update
|
|
24
|
+
|
|
25
|
+
Update installed xqa skills to the current CLI version.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
xqa update
|
|
29
|
+
```
|
|
30
|
+
|
|
23
31
|
### explore [prompt]
|
|
24
32
|
|
|
25
33
|
Run the explorer agent; omit prompt for a full breadth-first sweep.
|
|
@@ -31,44 +39,73 @@ xqa explore # breadth-first exploration
|
|
|
31
39
|
xqa explore "test the login flow" # focused exploration
|
|
32
40
|
xqa explore -v prompt,screen # verbose output for categories
|
|
33
41
|
xqa explore -v # verbose output for all categories
|
|
42
|
+
xqa explore -t 600 # override explorer timeout (seconds)
|
|
43
|
+
xqa explore --debug # log timing and event details to stderr
|
|
34
44
|
```
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
Flags:
|
|
47
|
+
|
|
48
|
+
- `-v, --verbose [categories]` — Log categories (prompt, tools, screen, memory). Default: all if flag is present without value.
|
|
49
|
+
- `-t, --timeout <seconds>` — Explorer timeout in seconds (overrides `QA_EXPLORE_TIMEOUT_SECONDS`).
|
|
50
|
+
- `--debug` — Log timing and event details to stderr.
|
|
37
51
|
|
|
38
52
|
### spec [spec-file]
|
|
39
53
|
|
|
40
54
|
Run the explorer agent against a spec file.
|
|
41
55
|
|
|
42
|
-
Loads a spec markdown file from `.xqa/specs/` (or an absolute path) and executes the agent against it.
|
|
56
|
+
Loads a spec markdown file from `.xqa/specs/` (or an absolute path) and executes the agent against it. Omit the argument to pick from available specs interactively.
|
|
43
57
|
|
|
44
58
|
```bash
|
|
45
59
|
xqa spec # interactive spec picker
|
|
46
|
-
xqa spec .xqa/specs/authentication.test.md
|
|
60
|
+
xqa spec .xqa/specs/authentication.test.md # explicit spec file
|
|
47
61
|
xqa spec -v tools,memory # verbose output
|
|
62
|
+
xqa spec --debug # debug logging
|
|
48
63
|
```
|
|
49
64
|
|
|
50
|
-
|
|
65
|
+
Flags:
|
|
66
|
+
|
|
67
|
+
- `-v, --verbose [categories]` — Same as explore.
|
|
68
|
+
- `--debug` — Log timing and event details to stderr.
|
|
51
69
|
|
|
52
70
|
Spec file format (YAML frontmatter + markdown):
|
|
53
71
|
|
|
54
72
|
```markdown
|
|
55
73
|
---
|
|
56
74
|
feature: 'Feature Name'
|
|
57
|
-
entry: 'Screen name or navigation path'
|
|
58
75
|
timeout: 300
|
|
59
76
|
---
|
|
60
77
|
|
|
61
78
|
# Spec content
|
|
62
79
|
```
|
|
63
80
|
|
|
81
|
+
Frontmatter fields: `feature` (required), `timeout` (optional, seconds).
|
|
82
|
+
|
|
83
|
+
### run
|
|
84
|
+
|
|
85
|
+
Run a test suite or a set of spec files in parallel across booted simulators.
|
|
86
|
+
|
|
87
|
+
Exactly one of `--suite` or `--spec` is required.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
xqa run --suite smoke # run .xqa/suites/smoke.suite.json
|
|
91
|
+
xqa run --spec 'specs/**/*.test.md' # run matching spec files
|
|
92
|
+
xqa run --suite smoke --debug # debug logging
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Flags:
|
|
96
|
+
|
|
97
|
+
- `--suite <name>` — Name of the suite (`<name>.suite.json`) under `.xqa/suites/`.
|
|
98
|
+
- `--spec <globs...>` — Glob patterns matching spec files, resolved from the xqa directory.
|
|
99
|
+
- `--debug` — Log timing and event details to stderr.
|
|
100
|
+
|
|
64
101
|
### review [findings-path]
|
|
65
102
|
|
|
66
103
|
Review findings and mark false positives.
|
|
67
104
|
|
|
68
|
-
Interactive session for triaging findings generated by explore or spec runs.
|
|
105
|
+
Interactive session for triaging findings generated by explore or spec runs. Mark findings as dismissed (with optional reason) or undo previous dismissals. Dismissals are written to `dismissals.json` next to the `.xqa` directory (override with `QA_DISMISSALS_PATH`). Defaults to the last findings path if omitted.
|
|
69
106
|
|
|
70
107
|
```bash
|
|
71
|
-
xqa review
|
|
108
|
+
xqa review # use last findings file
|
|
72
109
|
xqa review .xqa/output/findings-abc123.json # explicit path
|
|
73
110
|
```
|
|
74
111
|
|
|
@@ -93,26 +130,35 @@ xqa completion bash # generate bash completions
|
|
|
93
130
|
xqa completion zsh # generate zsh completions
|
|
94
131
|
```
|
|
95
132
|
|
|
96
|
-
### Suite
|
|
133
|
+
### Suite config
|
|
97
134
|
|
|
98
|
-
Suite
|
|
135
|
+
Suite files live at `.xqa/suites/<name>.suite.json` and declare the work items plus optional hooks.
|
|
99
136
|
|
|
100
137
|
```json
|
|
101
138
|
{
|
|
102
139
|
"specs": ["specs/send.test.md"],
|
|
140
|
+
"freestyle": [{ "prompt": "explore settings", "timeoutSeconds": 300 }],
|
|
103
141
|
"hooks": {
|
|
104
142
|
"beforeEach": {
|
|
105
143
|
"script": "qa/prepare-sim.mjs",
|
|
106
|
-
"env": { "APP_PROFILE": "funded" }
|
|
144
|
+
"env": { "APP_PROFILE": "funded" },
|
|
145
|
+
"timeoutSeconds": 120
|
|
107
146
|
}
|
|
108
147
|
}
|
|
109
148
|
}
|
|
110
149
|
```
|
|
111
150
|
|
|
151
|
+
Fields:
|
|
152
|
+
|
|
153
|
+
- `specs` (optional) — glob patterns resolved from the xqa directory.
|
|
154
|
+
- `freestyle` (optional) — either a positive integer (N empty entries) or an array of `{ prompt?, timeoutSeconds }` entries.
|
|
155
|
+
- At least one of `specs` or `freestyle` must resolve to a work item.
|
|
156
|
+
- `hooks.beforeEach` (optional) — runs before every work item on every simulator. Use for project-owned setup (wallet provisioning, cache warming, login seeding).
|
|
157
|
+
|
|
112
158
|
The hook script is invoked as a Node child process. It receives:
|
|
113
159
|
|
|
114
160
|
- Inherited `process.env`
|
|
115
|
-
- Suite-declared `env`
|
|
161
|
+
- Suite-declared `env` overlaid with reserved keys (reserved wins)
|
|
116
162
|
- Reserved xqa-owned keys: `XQA_SIM_UDID`, `XQA_ITEM_ID`, `XQA_ITEM_TYPE`, `XQA_ITEM_NAME`, `XQA_SUITE`, and (when item type is `spec`) `XQA_SPEC_PATH`
|
|
117
163
|
|
|
118
164
|
Suite-declared `env` cannot override reserved keys — the parser rejects such configs.
|
|
@@ -121,7 +167,7 @@ Contract:
|
|
|
121
167
|
|
|
122
168
|
- Exit 0 → proceed with item.
|
|
123
169
|
- Non-zero exit → item marked failed, `executeItem` skipped, counts toward simulator-unhealthy threshold.
|
|
124
|
-
- 120s timeout
|
|
170
|
+
- Default 120s timeout, overridable via `hooks.beforeEach.timeoutSeconds`.
|
|
125
171
|
- Honors the suite abort signal.
|
|
126
172
|
|
|
127
173
|
## Configuration
|
|
@@ -133,15 +179,17 @@ Configuration is loaded from environment variables and `.env.local`:
|
|
|
133
179
|
- `QA_RUN_ID` (optional) — Custom run identifier; defaults to auto-generated
|
|
134
180
|
- `QA_EXPLORE_TIMEOUT_SECONDS` (optional) — Exploration timeout in seconds
|
|
135
181
|
- `QA_BUILD_ENV` (optional) — Build environment: `dev` or `prod` (default: prod)
|
|
182
|
+
- `QA_DISMISSALS_PATH` (optional) — Override the dismissals file path used by `xqa review`
|
|
136
183
|
|
|
137
184
|
## Architecture
|
|
138
185
|
|
|
139
186
|
Key files and directories:
|
|
140
187
|
|
|
141
188
|
- `src/index.ts` — CLI entry point; wires commander commands and manages graceful shutdown via process locks
|
|
142
|
-
- `src/commands/` — Command implementations (init, explore, spec, review, analyse, completion)
|
|
143
|
-
- `src/
|
|
144
|
-
- `src/
|
|
189
|
+
- `src/commands/` — Command implementations (init, update, explore, spec, review, analyse, completion)
|
|
190
|
+
- `src/suite/` — Suite runner: config parsing, work-item building, worker pool, hooks
|
|
191
|
+
- `src/core/` — Pure functions: completion generation, verbose/timeout option parsing, last-path tracking
|
|
192
|
+
- `src/shell/` — I/O wrappers: app/explore context reading, debug logging, display factory, preflight, xqa directory discovery
|
|
145
193
|
- `src/config.ts`, `src/config-schema.ts` — Configuration loading and validation with Zod
|
|
146
194
|
- `src/review-session.ts` — Interactive finding review loop with dismissal tracking
|
|
147
195
|
- `src/spec-frontmatter.ts` — Spec markdown frontmatter parsing (YAML)
|
|
@@ -157,6 +205,8 @@ Core error discriminated unions:
|
|
|
157
205
|
- `XqaDirectoryError` — No .xqa directory found (XQA_NOT_INITIALIZED)
|
|
158
206
|
- `SpecFrontmatterError` — Malformed spec markdown (MISSING_FRONTMATTER, MISSING_FIELD, PARSE_ERROR)
|
|
159
207
|
- `LastPathError` — No findings path provided and no prior session (NO_ARG_AND_NO_STATE)
|
|
208
|
+
- `SuiteConfigError` — Suite config JSON malformed or schema-invalid (INVALID_SUITE_CONFIG)
|
|
209
|
+
- `HookError` — Suite hook failure (HOOK_SPAWN_FAILED, HOOK_EXIT_NONZERO, HOOK_TIMEOUT, HOOK_ABORTED)
|
|
160
210
|
|
|
161
211
|
## Development
|
|
162
212
|
|
|
@@ -231,20 +281,39 @@ src/
|
|
|
231
281
|
|
|
232
282
|
commands/
|
|
233
283
|
init-command.ts # Project initialization
|
|
284
|
+
update-command.ts # Skill updates
|
|
285
|
+
install-skills.ts # Bundled skill installer
|
|
234
286
|
explore-command.ts # Breadth-first exploration
|
|
235
287
|
spec-command.ts # Spec-based exploration
|
|
288
|
+
spec-resolver.ts # Spec file discovery and parsing
|
|
236
289
|
review-command.ts # Finding triage workflow
|
|
237
290
|
analyse-command.ts # Video analysis
|
|
238
291
|
completion-command.ts # Shell completion generation
|
|
292
|
+
item-events.ts # Start/complete/fail event emitters
|
|
239
293
|
|
|
240
294
|
core/
|
|
241
295
|
parse-verbose.ts # Verbose flag parsing
|
|
296
|
+
parse-timeout-seconds.ts # Timeout flag parsing
|
|
242
297
|
completion-generator.ts # Bash/zsh completion script generation
|
|
243
298
|
last-path.ts # Last findings path tracking
|
|
244
299
|
|
|
245
300
|
shell/
|
|
246
301
|
app-context.ts # Read app.md and explore.md
|
|
247
302
|
xqa-directory.ts # Locate .xqa directory
|
|
303
|
+
preflight.ts # Environment preflight checks
|
|
304
|
+
ffmpeg-check.ts # ffmpeg availability probe
|
|
305
|
+
display-factory.ts # Solo and suite display factories
|
|
306
|
+
debug-logger.ts # Debug event logging
|
|
307
|
+
debug-agent-events.ts # Agent event debug formatter
|
|
308
|
+
debug-suite-events.ts # Suite event debug formatter
|
|
309
|
+
debug-logger-core.ts # Pure logging helpers
|
|
310
|
+
trigger-abort.ts # Abort signal plumbing
|
|
311
|
+
|
|
312
|
+
suite/
|
|
313
|
+
types.ts # Suite work-item and findings types
|
|
314
|
+
core/ # Pure: config parser, item builders, hook env, queue
|
|
315
|
+
shell/ # I/O: worker pool, hook runner, findings writer
|
|
316
|
+
commands/ # run-command, execute-item, suite-run-context
|
|
248
317
|
|
|
249
318
|
__tests__/
|
|
250
319
|
*.test.ts # Test files co-located with src/
|
package/dist/xqa.cjs
CHANGED
|
@@ -52349,25 +52349,15 @@ function createListAppsTool(udid = "booted") {
|
|
|
52349
52349
|
}
|
|
52350
52350
|
var DEFAULT_LONG_PRESS_DURATION_MS = 500;
|
|
52351
52351
|
var MS_PER_SECOND = 1e3;
|
|
52352
|
+
var DEFAULT_SWIPE_DURATION_SECONDS = 0.1;
|
|
52352
52353
|
var ENTER_KEY_CODE = "0x28";
|
|
52353
|
-
var TAP_SCHEMA = { x: external_exports.number(), y: external_exports.number() };
|
|
52354
|
-
var LONG_PRESS_SCHEMA = { x: external_exports.number(), y: external_exports.number(), duration: external_exports.number().optional() };
|
|
52355
|
-
var SWIPE_SCHEMA = {
|
|
52356
|
-
x_start: external_exports.number(),
|
|
52357
|
-
y_start: external_exports.number(),
|
|
52358
|
-
x_end: external_exports.number(),
|
|
52359
|
-
y_end: external_exports.number()
|
|
52360
|
-
};
|
|
52361
|
-
var TYPE_TEXT_SCHEMA = { text: external_exports.string(), submit: external_exports.boolean().optional() };
|
|
52362
|
-
var PRESS_BUTTON_SCHEMA = {
|
|
52363
|
-
button: external_exports.enum(["HOME", "LOCK", "SIRI", "SIDE_BUTTON", "APPLE_PAY"])
|
|
52364
|
-
};
|
|
52365
52354
|
function errorResult2(error48) {
|
|
52366
52355
|
return {
|
|
52367
52356
|
content: [{ type: "text", text: `Error: ${String(error48.cause)}` }],
|
|
52368
52357
|
isError: true
|
|
52369
52358
|
};
|
|
52370
52359
|
}
|
|
52360
|
+
var TAP_SCHEMA = { x: external_exports.number(), y: external_exports.number() };
|
|
52371
52361
|
function buildTapArguments(resolvedUdid, input) {
|
|
52372
52362
|
return ["ui", "tap", "--udid", resolvedUdid, String(input.x), String(input.y)];
|
|
52373
52363
|
}
|
|
@@ -52402,6 +52392,23 @@ async function handleDoubleTap(input) {
|
|
|
52402
52392
|
]
|
|
52403
52393
|
};
|
|
52404
52394
|
}
|
|
52395
|
+
function createTapTool(udid = "booted") {
|
|
52396
|
+
return _x(
|
|
52397
|
+
"tap",
|
|
52398
|
+
"Tap on the screen at the given coordinates.",
|
|
52399
|
+
TAP_SCHEMA,
|
|
52400
|
+
async ({ x, y: y6 }) => handleTap({ udid, x, y: y6 })
|
|
52401
|
+
);
|
|
52402
|
+
}
|
|
52403
|
+
function createDoubleTapTool(udid = "booted") {
|
|
52404
|
+
return _x(
|
|
52405
|
+
"double_tap",
|
|
52406
|
+
"Double-tap on the screen at the given coordinates.",
|
|
52407
|
+
TAP_SCHEMA,
|
|
52408
|
+
async ({ x, y: y6 }) => handleDoubleTap({ udid, x, y: y6 })
|
|
52409
|
+
);
|
|
52410
|
+
}
|
|
52411
|
+
var LONG_PRESS_SCHEMA = { x: external_exports.number(), y: external_exports.number(), duration: external_exports.number().optional() };
|
|
52405
52412
|
function buildLongPressArguments(resolvedUdid, input) {
|
|
52406
52413
|
return [
|
|
52407
52414
|
"ui",
|
|
@@ -52430,8 +52437,39 @@ async function handleLongPress(input) {
|
|
|
52430
52437
|
]
|
|
52431
52438
|
};
|
|
52432
52439
|
}
|
|
52440
|
+
function createLongPressTool(udid = "booted") {
|
|
52441
|
+
return _x(
|
|
52442
|
+
"long_press",
|
|
52443
|
+
"Long-press on the screen at the given coordinates.",
|
|
52444
|
+
LONG_PRESS_SCHEMA,
|
|
52445
|
+
async ({ x, y: y6, duration: duration3 }) => handleLongPress({ udid, x, y: y6, duration: duration3 ?? DEFAULT_LONG_PRESS_DURATION_MS })
|
|
52446
|
+
);
|
|
52447
|
+
}
|
|
52448
|
+
var SWIPE_SCHEMA = {
|
|
52449
|
+
x_start: external_exports.number(),
|
|
52450
|
+
y_start: external_exports.number(),
|
|
52451
|
+
x_end: external_exports.number(),
|
|
52452
|
+
y_end: external_exports.number(),
|
|
52453
|
+
duration: external_exports.number().positive().optional().describe(
|
|
52454
|
+
"Seconds for the full gesture. Velocity = distance / duration \u2014 raise duration at fixed distance to slow the gesture and reduce momentum. Default 0.1s (flick) works for short lists; raise to 0.4\u20130.8 for controlled scrolling on long lists where the flick overshoots."
|
|
52455
|
+
),
|
|
52456
|
+
delta: external_exports.number().positive().optional().describe(
|
|
52457
|
+
"Pixel distance between interpolated touch points along the swipe path. Smaller values (e.g. 5) produce a denser event stream \u2014 smoother motion and more controllable stop-velocity, recommended when combining with a raised duration to tame long-list overshoot. Larger values produce coarser strokes. Omit to use idb defaults."
|
|
52458
|
+
)
|
|
52459
|
+
};
|
|
52460
|
+
function toSwipeInput(udid, params) {
|
|
52461
|
+
return {
|
|
52462
|
+
udid,
|
|
52463
|
+
xStart: params.x_start,
|
|
52464
|
+
yStart: params.y_start,
|
|
52465
|
+
xEnd: params.x_end,
|
|
52466
|
+
yEnd: params.y_end,
|
|
52467
|
+
duration: params.duration ?? DEFAULT_SWIPE_DURATION_SECONDS,
|
|
52468
|
+
delta: params.delta
|
|
52469
|
+
};
|
|
52470
|
+
}
|
|
52433
52471
|
function buildSwipeArguments(resolvedUdid, input) {
|
|
52434
|
-
|
|
52472
|
+
const arguments_ = [
|
|
52435
52473
|
"ui",
|
|
52436
52474
|
"swipe",
|
|
52437
52475
|
"--udid",
|
|
@@ -52439,8 +52477,14 @@ function buildSwipeArguments(resolvedUdid, input) {
|
|
|
52439
52477
|
String(input.xStart),
|
|
52440
52478
|
String(input.yStart),
|
|
52441
52479
|
String(input.xEnd),
|
|
52442
|
-
String(input.yEnd)
|
|
52480
|
+
String(input.yEnd),
|
|
52481
|
+
"--duration",
|
|
52482
|
+
String(input.duration)
|
|
52443
52483
|
];
|
|
52484
|
+
if (input.delta !== void 0) {
|
|
52485
|
+
arguments_.push("--delta", String(input.delta));
|
|
52486
|
+
}
|
|
52487
|
+
return arguments_;
|
|
52444
52488
|
}
|
|
52445
52489
|
async function handleSwipe(input) {
|
|
52446
52490
|
const result = await resolveUdid(input.udid).andThen(
|
|
@@ -52458,6 +52502,15 @@ async function handleSwipe(input) {
|
|
|
52458
52502
|
]
|
|
52459
52503
|
};
|
|
52460
52504
|
}
|
|
52505
|
+
function createSwipeTool(udid = "booted") {
|
|
52506
|
+
return _x(
|
|
52507
|
+
"swipe",
|
|
52508
|
+
"Swipe on the screen from one point to another. Default duration 0.1s produces a flick \u2014 use for scrolling lists, dismissing sheets, triggering paging. Use duration 0.5+ for slow drag (reorder, pan). For long lists where default flick overshoots: shorten swipe distance AND raise duration to 0.4\u20130.8 to lower velocity; optionally lower delta for denser touch events and a more controllable stop. Omitting duration uses the flick default.",
|
|
52509
|
+
SWIPE_SCHEMA,
|
|
52510
|
+
async (params) => handleSwipe(toSwipeInput(udid, params))
|
|
52511
|
+
);
|
|
52512
|
+
}
|
|
52513
|
+
var TYPE_TEXT_SCHEMA = { text: external_exports.string(), submit: external_exports.boolean().optional() };
|
|
52461
52514
|
async function handleTypeText({ udid, text, submit }) {
|
|
52462
52515
|
const result = await resolveUdid(udid).andThen((resolvedUdid) => {
|
|
52463
52516
|
const typeResult = runCommand("idb", ["ui", "text", "--udid", resolvedUdid, text]);
|
|
@@ -52473,6 +52526,17 @@ async function handleTypeText({ udid, text, submit }) {
|
|
|
52473
52526
|
}
|
|
52474
52527
|
return { content: [{ type: "text", text: `Typed: ${text}` }] };
|
|
52475
52528
|
}
|
|
52529
|
+
function createTypeTextTool(udid = "booted") {
|
|
52530
|
+
return _x(
|
|
52531
|
+
"type_text",
|
|
52532
|
+
"Type text into the currently focused element.",
|
|
52533
|
+
TYPE_TEXT_SCHEMA,
|
|
52534
|
+
async ({ text, submit }) => handleTypeText({ udid, text, submit: submit ?? false })
|
|
52535
|
+
);
|
|
52536
|
+
}
|
|
52537
|
+
var PRESS_BUTTON_SCHEMA = {
|
|
52538
|
+
button: external_exports.enum(["HOME", "LOCK", "SIRI", "SIDE_BUTTON", "APPLE_PAY"])
|
|
52539
|
+
};
|
|
52476
52540
|
async function handlePressButton(udid, button) {
|
|
52477
52541
|
const result = await resolveUdid(udid).andThen(
|
|
52478
52542
|
(resolvedUdid) => runCommand("idb", ["ui", "button", "--udid", resolvedUdid, button])
|
|
@@ -52482,51 +52546,6 @@ async function handlePressButton(udid, button) {
|
|
|
52482
52546
|
}
|
|
52483
52547
|
return { content: [{ type: "text", text: `Pressed button: ${button}` }] };
|
|
52484
52548
|
}
|
|
52485
|
-
function createTapTool(udid = "booted") {
|
|
52486
|
-
return _x(
|
|
52487
|
-
"tap",
|
|
52488
|
-
"Tap on the screen at the given coordinates.",
|
|
52489
|
-
TAP_SCHEMA,
|
|
52490
|
-
async ({ x, y: y6 }) => handleTap({ udid, x, y: y6 })
|
|
52491
|
-
);
|
|
52492
|
-
}
|
|
52493
|
-
function createDoubleTapTool(udid = "booted") {
|
|
52494
|
-
return _x(
|
|
52495
|
-
"double_tap",
|
|
52496
|
-
"Double-tap on the screen at the given coordinates.",
|
|
52497
|
-
TAP_SCHEMA,
|
|
52498
|
-
async ({ x, y: y6 }) => handleDoubleTap({ udid, x, y: y6 })
|
|
52499
|
-
);
|
|
52500
|
-
}
|
|
52501
|
-
function createLongPressTool(udid = "booted") {
|
|
52502
|
-
return _x(
|
|
52503
|
-
"long_press",
|
|
52504
|
-
"Long-press on the screen at the given coordinates.",
|
|
52505
|
-
LONG_PRESS_SCHEMA,
|
|
52506
|
-
async ({ x, y: y6, duration: duration3 }) => handleLongPress({ udid, x, y: y6, duration: duration3 ?? DEFAULT_LONG_PRESS_DURATION_MS })
|
|
52507
|
-
);
|
|
52508
|
-
}
|
|
52509
|
-
function createSwipeTool(udid = "booted") {
|
|
52510
|
-
return _x(
|
|
52511
|
-
"swipe",
|
|
52512
|
-
"Swipe on the screen from one point to another.",
|
|
52513
|
-
SWIPE_SCHEMA,
|
|
52514
|
-
async ({
|
|
52515
|
-
x_start,
|
|
52516
|
-
y_start,
|
|
52517
|
-
x_end,
|
|
52518
|
-
y_end
|
|
52519
|
-
}) => handleSwipe({ udid, xStart: x_start, yStart: y_start, xEnd: x_end, yEnd: y_end })
|
|
52520
|
-
);
|
|
52521
|
-
}
|
|
52522
|
-
function createTypeTextTool(udid = "booted") {
|
|
52523
|
-
return _x(
|
|
52524
|
-
"type_text",
|
|
52525
|
-
"Type text into the currently focused element.",
|
|
52526
|
-
TYPE_TEXT_SCHEMA,
|
|
52527
|
-
async ({ text, submit }) => handleTypeText({ udid, text, submit: submit ?? false })
|
|
52528
|
-
);
|
|
52529
|
-
}
|
|
52530
52549
|
function createPressButtonTool(udid = "booted") {
|
|
52531
52550
|
return _x(
|
|
52532
52551
|
"press_button",
|
|
@@ -62942,15 +62961,17 @@ At every reasoning step, maintain a mental ledger:
|
|
|
62942
62961
|
Consult the ledger before every action. Always prefer navigating to a QUEUE screen over a VISITED one.`;
|
|
62943
62962
|
var SESSION_START_RULE = `Before taking any other action \u2014 including initializing the Working State ledger or emitting findings \u2014 call \`view_ui\` once to observe the starting screen`;
|
|
62944
62963
|
var BACK_NAV_RULE = `After navigating forward to any new screen: attempt to return to the expected parent in PATH \u2014 consult App Knowledge first for the correct exit gesture on this screen, then try in order: (1) any visible back/close button, (2) OS back gesture, (3) swipe up, (4) swipe down, (5) swipe left, (6) swipe right \u2014 confirm return via \`screenshot\` if the parent is visually unambiguous, \`view_ui\` otherwise \u2014 only after ALL attempts fail emit a \`back-nav-failure\` finding, then navigate forward again to continue`;
|
|
62945
|
-
var STUCK_LOOP_RULE = `Stuck loop: emit a \`stuck-loop\` finding when any of these occur: (1) \`view_ui\` returns the same \`<screen_id>\` across 3 or more consecutive \`view_ui\` calls, (2) the same element has been tapped more than twice with no screen change confirmed by \`view_ui\`, (3) PATH shows the same screen at two non-adjacent positions \u2014 before emitting, try one alternative action (scroll, long-press, swipe) to rule out a gesture mismatch \u2014 note: \`screenshot\`-only calls do not update the stuck-loop counter; only \`view_ui\` calls count
|
|
62964
|
+
var STUCK_LOOP_RULE = `Stuck loop: emit a \`stuck-loop\` finding when any of these signals occur: (1) \`view_ui\` returns the same \`<screen_id>\` across 3 or more consecutive \`view_ui\` calls, (2) the same element has been tapped more than twice with no screen change confirmed by \`view_ui\`, (3) PATH shows the same screen at two non-adjacent positions \u2014 i.e. you visited a screen, left it, and returned. Example: \`Portfolio \u2192 Asset \u2192 Swap \u2192 Asset\` (Asset revisited). Indicates circular navigation. NOT tripped by adjacent repeats like \`Portfolio \u2192 Asset \u2192 Asset\` (normal state refresh), (4) a scrollable list's element positions \u2014 x for horizontal, y for vertical \u2014 are identical across 2 or more consecutive swipe+\`view_ui\` cycles (zero-delta scroll stall) \u2014 before emitting for (1)\u2013(3), try one alternative action (scroll, long-press, swipe) to rule out a gesture mismatch \u2014 before emitting for (4), baseline element positions via \`view_ui\`, then vary one spatial parameter on the next attempt: shift swipe start coord by \u226540% of visible container dimension, OR reverse direction to probe opposite bound, OR extend throw to span \u226580% of viewport dimension \u2014 then \`view_ui\` and compare. Example: if tab bar positions \`Tokens(-31) ETH(65) ... Tron(352)\` are unchanged across 2 swipes starting near x=350, shift start to x=10 or x=395 and extend end past the opposite viewport edge. If delta still zero, emit \`stuck-loop\` with axis, before/after positions, and variation attempted \u2014 note: \`screenshot\`-only calls do not update the stuck-loop counter; only \`view_ui\` calls count. Zero-delta scroll stall is not a separate finding type \u2014 report as \`stuck-loop\`.`;
|
|
62946
62965
|
var LOADING_STATE_RULE = `Transient loading state: when the screen shows spinners, skeleton screens, progress bars, "Loading..." text, or placeholder content NOT described in spec or app context \u2014 use \`screenshot\` to poll for resolution (up to 3 retries); switch to \`view_ui\` only on the final check or when you need element data to act \u2014 if loading persists after 3 retries, proceed with what is visible; if spec or app context explicitly describes a loading screen as a step, do not retry \u2014 call \`view_ui\` and assert normally`;
|
|
62947
62966
|
var EXPECTED_CONTENT_MISSING_RULE = `Expected content missing: when \`view_ui\` shows no loading indicator yet omits an element named or strongly implied by spec or app context \u2014 and its absence is not semantically consistent with the current screen \u2014 call \`wait_seconds\` with 2\u20135 seconds and retry \`view_ui\` up to 2 times; if element remains absent, emit a \`missing-content\` finding stating what was expected and what was observed`;
|
|
62948
62967
|
var CLIPPED_ELEMENT_RULE = `Never tap an element tagged \`[clipped-top]\`, \`[clipped-bottom]\`, \`[clipped-left]\`, or \`[clipped-right]\` \u2014 scroll to fully reveal it first, then re-call \`view_ui\` before tapping`;
|
|
62949
|
-
var SCROLL_FOLD_RULE = `Scrollable lists: elements
|
|
62968
|
+
var SCROLL_FOLD_RULE = `Scrollable lists: elements outside the visible viewport are absent from the a11y tree by design \u2014 this applies to elements below the fold in vertical lists AND elements clipped off-left or off-right in horizontal lists \u2014 scroll or swipe in the appropriate axis to reveal before asserting presence or absence; never emit a finding solely because list items, rows, or tabs are missing from the tree on a scrollable screen; if swipe attempts yield no position change across 2+ cycles, apply the scroll-stall path in STUCK_LOOP_RULE.`;
|
|
62950
62969
|
var FINDING_TAXONOMY_SECTION = `## Finding Types
|
|
62951
62970
|
|
|
62952
62971
|
You may emit only these trigger types: \`back-nav-failure\`, \`dead-end\`, \`stuck-modal\`, \`stuck-loop\`, \`missing-a11y-element\`, \`missing-content\`, \`spec-deviation\`, \`destructive-only-exit\`. Do NOT emit \`design-system-violation\`, \`motion-regression\`, \`continuity-regression\`, \`interaction-regression\`, or \`loading-regression\` \u2014 those belong to other agents.`;
|
|
62953
62972
|
var A11Y_FALLBACK_RULE = `Missing a11y element: if you intend to tap or interact with a UI element and that element is absent from the most recent \`view_ui\` a11y tree \u2014 visible in the screenshot does NOT imply interactable; the a11y tree is authoritative \u2014 do NOT estimate its coordinates from the screenshot, do NOT attempt any pixel-based tap, do NOT retry at different coordinates, do NOT long-press or swipe in the element's visual region as a fallback; a failed pixel tap is never an \`interaction-regression\` \u2014 it is a \`missing-a11y-element\` \u2014 instead, emit a \`missing-a11y-element\` finding that states: (1) your intent (what you were trying to do), (2) the approximate visual region where the element appeared (coords/size from the screenshot), (3) nearby labeled elements from the a11y tree that serve as landmarks \u2014 then continue: in freestyle mode keep exploring other reachable screens; in spec mode advance to the next step`;
|
|
62973
|
+
var OUTCOME_LITERAL_RULE = `When verifying a step outcome or assertion, interpret all quantifiers literally and apply them exhaustively: any keyword that imposes a universal or count-bound constraint \u2014 including but not limited to \`only\`, \`all\`, \`every\`, \`each\`, \`both\`, \`no\`, \`none\`, \`neither\`, \`exactly N\`, \`at least N\`, \`fewer than N\`, \`more than N\` \u2014 a single counter-example observed in \`view_ui\` or \`screenshot\` constitutes a failed constraint; this rule applies only when the outcome text contains a universal or count-bound quantifier; positive-existence outcomes (\`X appears\`, \`Y becomes active\`, \`screen is visible\`) are not subject to counter-example scanning and are governed by SPEC_STEP_READING_SECTION; on a scrollable list, scroll through the full list before concluding pass or fail on a universal quantifier \u2014 apply SCROLL_FOLD_RULE to reveal all items, then evaluate across the complete visible set; if the screen shows any loading indicator at the time of observation, apply LOADING_STATE_RULE first and re-verify after resolution before emitting; do NOT soften (\`mostly\`, \`essentially\`, \`largely\`) and do NOT narrow the target class post-hoc to exclude the counter-example; when the counter-evidence is an element absent from the a11y tree, A11Y_FALLBACK_RULE takes precedence and determines the finding type (\`missing-a11y-element\`) \u2014 OUTCOME_LITERAL_RULE governs unwanted-presence violations only; if one item violates the constraint, emit \`spec-deviation\` immediately with: (a) the literal outcome text, (b) the specific element(s) that violate it, (c) the constraint keyword that is broken.`;
|
|
62974
|
+
var ANTI_RATIONALIZATION_RULE = `During outcome verification, monitor your own reasoning for reconciliation hypotheses: if you generate any reasoning that re-frames, redefines, or reinterprets the observed counter-example or target class in order to produce agreement with the spec outcome \u2014 regardless of phrasing \u2014 treat that reasoning as a deviation signal, not a resolution; stop, do NOT mark the step complete, and emit \`spec-deviation\` with: (a) the literal outcome text, (b) the specific observation that triggered the hypothesis, (c) the reconciliation reasoning itself verbatim; before marking any quantifier-bearing outcome complete, state explicitly in your reasoning: \`No reconciliation hypothesis generated. Counter-examples found: [list or none].\` If you cannot make that statement honestly, a hypothesis exists \u2014 emit \`spec-deviation\`; when outcome verification is ambiguous and no reconciliation hypothesis is generated, still default to emitting \`spec-deviation\`; silence is not a pass.`;
|
|
62954
62975
|
var WHAT_TO_TEST_SECTION = `## What to Test
|
|
62955
62976
|
|
|
62956
62977
|
Test navigation elements first, interactions second.
|
|
@@ -63043,6 +63064,8 @@ var SPEC_RULES_SECTION = `## Rules
|
|
|
63043
63064
|
- ${CLIPPED_ELEMENT_RULE}
|
|
63044
63065
|
- ${SCROLL_FOLD_RULE}
|
|
63045
63066
|
- ${A11Y_FALLBACK_RULE}
|
|
63067
|
+
- ${OUTCOME_LITERAL_RULE}
|
|
63068
|
+
- ${ANTI_RATIONALIZATION_RULE}
|
|
63046
63069
|
- Each item in \`**Assertions**\` is a mandatory pass/fail check \u2014 verify using \`view_ui\` when the assertion targets an element attribute, label, or presence in the tree; use \`screenshot\` when the assertion is purely visual; if neither can confirm, emit a \`spec-deviation\` finding based on what is observable
|
|
63047
63070
|
- Flag crash dialogs, unexpected system errors, or navigation failures that occur as a direct result of executing a spec step; if you observe a visibly broken element in passing while navigating, note it without interacting with it`;
|
|
63048
63071
|
function buildSpecModeBody({
|
|
@@ -76151,6 +76174,9 @@ var DEFAULT_FREESTYLE_TIMEOUT_SECONDS2 = 300;
|
|
|
76151
76174
|
function buildDeviceInstruction(simulatorUdid) {
|
|
76152
76175
|
return `You MUST use device "${simulatorUdid}" for ALL mobile tool calls. The simulator UDID is already configured \u2014 use it directly.`;
|
|
76153
76176
|
}
|
|
76177
|
+
function composeAppContext(parts) {
|
|
76178
|
+
return parts.filter((part) => part !== void 0 && part.length > 0).join("\n\n");
|
|
76179
|
+
}
|
|
76154
76180
|
function ensureWorkerCwd(simulatorUdid) {
|
|
76155
76181
|
const workerDirectory = import_node_path21.default.join(import_node_os4.default.tmpdir(), "xqa-workers", simulatorUdid);
|
|
76156
76182
|
(0, import_node_fs11.mkdirSync)(workerDirectory, { recursive: true });
|
|
@@ -76164,7 +76190,7 @@ function resolveFreestyleTimeout(item, config3) {
|
|
|
76164
76190
|
}
|
|
76165
76191
|
function buildFreestylePipelineConfig(input) {
|
|
76166
76192
|
const { item, context, signal, simulatorUdid, onEvent } = input;
|
|
76167
|
-
const { config: config3, xqaDirectory, runId, date: date5 } = context;
|
|
76193
|
+
const { config: config3, xqaDirectory, runId, date: date5, appContext } = context;
|
|
76168
76194
|
return {
|
|
76169
76195
|
outputDir: import_node_path21.default.join(xqaDirectory, "output", item.id),
|
|
76170
76196
|
runId,
|
|
@@ -76177,9 +76203,11 @@ function buildFreestylePipelineConfig(input) {
|
|
|
76177
76203
|
mcpServers: createDefaultMcpServers(simulatorUdid),
|
|
76178
76204
|
allowedTools: ALLOWED_TOOLS,
|
|
76179
76205
|
timeoutMs: resolveFreestyleTimeout(item, config3) * MS_PER_SECOND4,
|
|
76180
|
-
appContext:
|
|
76181
|
-
|
|
76182
|
-
|
|
76206
|
+
appContext: composeAppContext([
|
|
76207
|
+
buildDeviceInstruction(simulatorUdid),
|
|
76208
|
+
appContext,
|
|
76209
|
+
item.prompt
|
|
76210
|
+
]),
|
|
76183
76211
|
buildEnv: config3.QA_BUILD_ENV,
|
|
76184
76212
|
cwd: ensureWorkerCwd(simulatorUdid)
|
|
76185
76213
|
}
|
|
@@ -76190,7 +76218,7 @@ function executeFreestyleItem(input) {
|
|
|
76190
76218
|
}
|
|
76191
76219
|
function buildSpecPipelineConfig(input) {
|
|
76192
76220
|
const { item, context, signal, simulatorUdid, onEvent } = input;
|
|
76193
|
-
const { config: config3, xqaDirectory, runId } = context;
|
|
76221
|
+
const { config: config3, xqaDirectory, runId, appContext } = context;
|
|
76194
76222
|
return {
|
|
76195
76223
|
outputDir: import_node_path21.default.join(xqaDirectory, "output", item.id),
|
|
76196
76224
|
runId,
|
|
@@ -76202,7 +76230,7 @@ function buildSpecPipelineConfig(input) {
|
|
|
76202
76230
|
specFiles: [item.specPath],
|
|
76203
76231
|
mcpServers: createDefaultMcpServers(simulatorUdid),
|
|
76204
76232
|
allowedTools: ALLOWED_TOOLS,
|
|
76205
|
-
appContext: buildDeviceInstruction(simulatorUdid),
|
|
76233
|
+
appContext: composeAppContext([buildDeviceInstruction(simulatorUdid), appContext]),
|
|
76206
76234
|
buildEnv: config3.QA_BUILD_ENV,
|
|
76207
76235
|
cwd: ensureWorkerCwd(simulatorUdid)
|
|
76208
76236
|
}
|
|
@@ -76278,17 +76306,30 @@ function deriveSuiteIdFromMode(mode) {
|
|
|
76278
76306
|
}
|
|
76279
76307
|
return deriveSuiteId({ mode: "spec", globs: mode.globs });
|
|
76280
76308
|
}
|
|
76309
|
+
async function loadAppContext(xqaDirectory) {
|
|
76310
|
+
const result = await readAppContext(xqaDirectory);
|
|
76311
|
+
if (result.isErr()) {
|
|
76312
|
+
const cause = result.error.cause instanceof Error ? result.error.cause.message : JSON.stringify(result.error.cause);
|
|
76313
|
+
process.stderr.write(`Failed to read app context: ${result.error.type}
|
|
76314
|
+
${cause}
|
|
76315
|
+
`);
|
|
76316
|
+
return void 0;
|
|
76317
|
+
}
|
|
76318
|
+
return result.value;
|
|
76319
|
+
}
|
|
76281
76320
|
async function buildSuiteRunContext(input) {
|
|
76282
76321
|
const suiteId = deriveSuiteIdFromMode(input.mode);
|
|
76283
76322
|
const date5 = (/* @__PURE__ */ new Date()).toISOString().slice(0, ISO_DATE_LENGTH3);
|
|
76284
76323
|
const outputDirectory = import_node_path22.default.join(input.xqaDirectory, "output");
|
|
76285
76324
|
const existingDirectories = await listRunDirectories({ outputDirectory, suiteId, date: date5 });
|
|
76286
76325
|
const runId = computeNextRunId(existingDirectories);
|
|
76326
|
+
const appContext = await loadAppContext(input.xqaDirectory);
|
|
76287
76327
|
const context = {
|
|
76288
76328
|
config: input.config,
|
|
76289
76329
|
xqaDirectory: input.xqaDirectory,
|
|
76290
76330
|
runId,
|
|
76291
|
-
date: date5
|
|
76331
|
+
date: date5,
|
|
76332
|
+
appContext
|
|
76292
76333
|
};
|
|
76293
76334
|
return { suiteId, date: date5, outputDirectory, runId, context };
|
|
76294
76335
|
}
|
|
@@ -76482,7 +76523,7 @@ function resolveXqaDirectory() {
|
|
|
76482
76523
|
return result.value;
|
|
76483
76524
|
}
|
|
76484
76525
|
var program2 = new Command();
|
|
76485
|
-
program2.name("xqa").description("AI-powered QA agent CLI").version(`${"1.14.
|
|
76526
|
+
program2.name("xqa").description("AI-powered QA agent CLI").version(`${"1.14.2"}${false ? ` (dev build +${"5d87cb5"})` : ""}`);
|
|
76486
76527
|
program2.command("init").description("Initialize a new xqa project in the current directory").action(() => {
|
|
76487
76528
|
runInitCommand();
|
|
76488
76529
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/xqa",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22"
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"typescript": "^5.8.3",
|
|
27
27
|
"vitest": "^3.2.1",
|
|
28
28
|
"zod": "^3.0.0",
|
|
29
|
-
"@qa-agents/explorer": "0.0.0",
|
|
30
|
-
"@qa-agents/eslint-config": "0.0.0",
|
|
31
29
|
"@qa-agents/display": "0.0.0",
|
|
30
|
+
"@qa-agents/eslint-config": "0.0.0",
|
|
31
|
+
"@qa-agents/explorer": "0.0.0",
|
|
32
32
|
"@qa-agents/mobile-ios": "0.0.0",
|
|
33
33
|
"@qa-agents/pipeline": "0.0.0",
|
|
34
34
|
"@qa-agents/shared": "0.0.0",
|