@unotest/mobile 0.8.3 → 0.9.1

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.
@@ -163,16 +163,24 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
163
163
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
164
164
 
165
165
  // src/config/env.ts
166
- import { config as loadDotenv } from "dotenv";
166
+ import { applyEnvLayers as applyCoreEnvLayers } from "@unotest/core";
167
167
  import { z } from "zod";
168
- var ENV_FILE_PATH = "unotest/.env";
168
+
169
+ // src/runner/scenarios-dir.ts
170
+ import { runsDirFor, testDirFor, UNOTEST_DIR } from "@unotest/protocol";
171
+ var MOBILE_SUFFIX = "-mobile";
172
+ var MOBILE_TEST_DIR = testDirFor(MOBILE_SUFFIX);
173
+ var MOBILE_SCENARIOS_DIR = `${UNOTEST_DIR}/${MOBILE_TEST_DIR}`;
174
+ var MOBILE_RUNS_DIR = `${UNOTEST_DIR}/${runsDirFor(MOBILE_SUFFIX)}`;
175
+
176
+ // src/config/env.ts
169
177
  var loaded = false;
170
- function ensureLoaded() {
178
+ function applyEnvLayers() {
171
179
  if (loaded) return;
172
- loadDotenv({ path: ENV_FILE_PATH });
180
+ applyCoreEnvLayers(process.cwd(), MOBILE_SUFFIX);
173
181
  loaded = true;
174
182
  }
175
- __name(ensureLoaded, "ensureLoaded");
183
+ __name(applyEnvLayers, "applyEnvLayers");
176
184
  var EnvSchema = z.object({
177
185
  // APP_BUNDLE_ID — required at appLaunch / install time. We let the schema
178
186
  // accept it as optional so commands that don't touch the app (`doctor`,
@@ -219,7 +227,10 @@ var EnvSchema = z.object({
219
227
  // `exp+<APP_URL_SCHEME>://expo-development-client/?url=<METRO_URL>` after
220
228
  // `app_launch clean: true` to bypass the launcher. (Flow not yet wired.)
221
229
  EXPO_DEV_CLIENT: z.string().optional().transform((v) => v === "true" || v === "1"),
222
- SESSION_LOG_PATH: z.string().default("unotest/sessions/current.jsonl"),
230
+ // Dot-prefixed + target-suffixed per the M-10 artifact convention
231
+ // (`.sessions-mobile`, like `.runs-mobile`): one `.gitignore` wildcard
232
+ // covers all runtime debris, and web/mobile artifacts never mix.
233
+ SESSION_LOG_PATH: z.string().default("unotest/.sessions-mobile/current.jsonl"),
223
234
  // Explicit kill-switch for session recording. When "1"/"true", or when
224
235
  // SESSION_LOG_PATH is empty, buildApp wires a NoopSessionRecorder. Used
225
236
  // by evals harness and any consumer that wants the MCP server to make
@@ -228,10 +239,10 @@ var EnvSchema = z.object({
228
239
  // When true, recorder writes the FULL tool result alongside the
229
240
  // truncated preview. Off by default — snapshots can be megabytes.
230
241
  SESSION_LOG_FULL: z.string().optional().transform((v) => v === "1" || v === "true"),
231
- ARTIFACTS_DIR: z.string().default("unotest/artifacts"),
242
+ ARTIFACTS_DIR: z.string().default("unotest/.artifacts-mobile"),
232
243
  // Where ExplorationService persists per-session JSONL recording logs.
233
244
  // Default: <ARTIFACTS_DIR>/explorations. Folded into the gitignored
234
- // `unotest/artifacts/` tree by the init template.
245
+ // `unotest/.artifacts-mobile/` tree by the init template.
235
246
  EXPLORATIONS_DIR: z.string().optional(),
236
247
  // WDA per-slot port mapping (D-13 parallel multi-device). Stored as a
237
248
  // comma-separated `slot=port` list, e.g. "A=8100,B=8101". Each slot present
@@ -255,7 +266,7 @@ var EnvSchema = z.object({
255
266
  var cached = null;
256
267
  function loadEnv() {
257
268
  if (cached) return cached;
258
- ensureLoaded();
269
+ applyEnvLayers();
259
270
  const parsed = EnvSchema.safeParse(process.env);
260
271
  if (!parsed.success) {
261
272
  const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
@@ -2164,1963 +2175,301 @@ var TreeInspector = class {
2164
2175
  }
2165
2176
  };
2166
2177
 
2167
- // vendor/dsl/parser/lib/array-value.ts
2168
- var ArrayValue = class _ArrayValue {
2169
- static {
2170
- __name(this, "ArrayValue");
2171
- }
2172
- elements;
2173
- constructor(sizeOrElements) {
2174
- if (typeof sizeOrElements === "number") {
2175
- this.elements = new Array(sizeOrElements).fill(void 0);
2176
- } else if (Array.isArray(sizeOrElements)) {
2177
- this.elements = [...sizeOrElements];
2178
- } else {
2179
- this.elements = [...sizeOrElements.elements];
2180
- }
2181
- }
2182
- get(index) {
2183
- return this.elements[index];
2184
- }
2185
- set(index, value) {
2186
- this.elements[index] = value;
2187
- }
2188
- asNumber() {
2189
- throw new Error("Cannot cast array to number");
2190
- }
2191
- clone() {
2192
- return new _ArrayValue(this.elements.map((e) => e.clone()));
2193
- }
2194
- asString() {
2195
- return `[${this.elements.map((e) => e.asString()).join(", ")}]`;
2196
- }
2197
- toString() {
2198
- return this.asString();
2199
- }
2200
- };
2201
-
2202
- // vendor/dsl/parser/lib/number-value.ts
2203
- var NumberValue = class _NumberValue {
2178
+ // src/dsl/ast-executor.ts
2179
+ import {
2180
+ ExecutionWalker
2181
+ } from "@unotest/dsl/executor";
2182
+ import { UnsupportedAstNodeError } from "@unotest/dsl/executor";
2183
+ var AstExecutor = class {
2204
2184
  static {
2205
- __name(this, "NumberValue");
2206
- }
2207
- static ZERO = new _NumberValue(0);
2208
- value;
2209
- constructor(value) {
2210
- if (typeof value === "boolean") {
2211
- this.value = value ? 1 : 0;
2212
- } else {
2213
- this.value = value;
2214
- }
2215
- }
2216
- clone() {
2217
- return new _NumberValue(this.value);
2218
- }
2219
- asNumber() {
2220
- return this.value;
2185
+ __name(this, "AstExecutor");
2221
2186
  }
2222
- asString() {
2223
- return this.value.toString();
2187
+ walker;
2188
+ constructor(registry) {
2189
+ this.walker = new ExecutionWalker(
2190
+ registry
2191
+ );
2224
2192
  }
2225
- toString() {
2226
- return this.asString();
2193
+ run(program, testName, runtime, opts, helpers) {
2194
+ return this.walker.run(
2195
+ program,
2196
+ testName,
2197
+ runtime,
2198
+ { resumeRetriesFailedStep: true, ...opts },
2199
+ helpers
2200
+ );
2227
2201
  }
2228
2202
  };
2229
2203
 
2230
- // vendor/dsl/parser/lib/functions.ts
2231
- var Functions = class {
2232
- static {
2233
- __name(this, "Functions");
2234
- }
2235
- static functions = /* @__PURE__ */ new Map([
2236
- // prettier-ignore
2237
- ["cos", { execute(...args) {
2238
- if (args.length !== 1) {
2239
- throw new Error("One argument expected");
2240
- }
2241
- return new NumberValue(Math.cos(args[0].asNumber()));
2242
- } }],
2243
- // prettier-ignore
2244
- ["echo", { execute(...args) {
2245
- for (const arg of args) {
2246
- console.log(arg.asString());
2247
- }
2248
- return NumberValue.ZERO;
2249
- } }],
2250
- // prettier-ignore
2251
- ["highlightBlock", { execute(...args) {
2252
- return NumberValue.ZERO;
2253
- } }],
2254
- // prettier-ignore
2255
- ["setPage", { execute(...args) {
2256
- return NumberValue.ZERO;
2257
- } }],
2258
- // prettier-ignore
2259
- ["afterBlock", { execute(...args) {
2260
- return NumberValue.ZERO;
2261
- } }],
2262
- // prettier-ignore
2263
- ["open", { execute(...args) {
2264
- return NumberValue.ZERO;
2265
- } }],
2266
- // prettier-ignore
2267
- ["String", { execute(...args) {
2268
- return NumberValue.ZERO;
2269
- } }]
2270
- ]);
2271
- static isExists(key) {
2272
- return this.functions.has(key);
2273
- }
2274
- static get(key) {
2275
- const f = this.functions.get(key);
2276
- if (!f) {
2277
- throw new TypeError(`Unknown function: ${key}`);
2278
- }
2279
- return f;
2280
- }
2281
- static set(name, value) {
2282
- this.functions.set(name, value);
2283
- }
2284
- };
2204
+ // src/dsl/linter.ts
2205
+ import {
2206
+ AssignmentStatement,
2207
+ ArrayAccessExpression,
2208
+ ArrayAssignmentStatement,
2209
+ ArrayExpression,
2210
+ BinaryExpression,
2211
+ BlockStatement,
2212
+ BreakStatement,
2213
+ ConditionalExpression,
2214
+ ContinueStatement,
2215
+ DoWhileStatement,
2216
+ ForStatement,
2217
+ FunctionDefineStatement,
2218
+ FunctionStatement,
2219
+ FunctionalExpression,
2220
+ IfStatement,
2221
+ IncrementExpression,
2222
+ IncrementStatement,
2223
+ MemberCallExpression,
2224
+ MetaBlockStatement,
2225
+ ObjectExpression,
2226
+ PropertyAccessExpression,
2227
+ PrintStatement,
2228
+ ReturnStatement,
2229
+ StepStatement,
2230
+ UnaryExpression,
2231
+ ValueExpression,
2232
+ VarStatement,
2233
+ VariableExpression,
2234
+ WhileStatement
2235
+ } from "@unotest/dsl";
2236
+ import { validateDsl } from "@unotest/dsl/validator";
2285
2237
 
2286
- // vendor/dsl/parser/lib/object-value.ts
2287
- var ObjectValue = class _ObjectValue {
2288
- static {
2289
- __name(this, "ObjectValue");
2290
- }
2291
- properties;
2292
- constructor(properties = /* @__PURE__ */ new Map()) {
2293
- this.properties = properties;
2294
- }
2295
- get(key) {
2296
- return this.properties.get(key);
2297
- }
2298
- set(key, value) {
2299
- this.properties.set(key, value);
2300
- }
2301
- asNumber() {
2302
- return this.properties.size;
2303
- }
2304
- asString() {
2305
- const entries = Array.from(this.properties.entries()).map(([key, value]) => `${key}: ${value.asString()}`).join(", ");
2306
- return `{${entries}}`;
2307
- }
2308
- clone() {
2309
- const clonedProperties = /* @__PURE__ */ new Map();
2310
- this.properties.forEach((value, key) => {
2311
- clonedProperties.set(key, value.clone());
2312
- });
2313
- return new _ObjectValue(clonedProperties);
2314
- }
2315
- getProperties() {
2316
- return this.properties;
2317
- }
2238
+ // src/dsl/mobile-dsl-registry.ts
2239
+ var ARG_KIND = {
2240
+ // Mobile's loose types map onto the engine's lenient predicates —
2241
+ // variables / helper-call results of unknown kind always pass, like the
2242
+ // old E6 "only flag the unambiguous" policy. `selector` maps to the
2243
+ // STRICT locator kind: mobile selectors only come from selector
2244
+ // functions (getByTestId/getByText/…), never bare strings.
2245
+ string: "stringLike",
2246
+ number: "number",
2247
+ boolean: "boolLike",
2248
+ selector: "strictLocatorLike",
2249
+ any: "any"
2318
2250
  };
2319
-
2320
- // vendor/dsl/parser/lib/string-value.ts
2321
- var StringValue = class _StringValue {
2322
- static {
2323
- __name(this, "StringValue");
2324
- }
2325
- value;
2326
- constructor(value) {
2327
- this.value = value;
2328
- }
2329
- clone() {
2330
- return new _StringValue(this.value);
2331
- }
2332
- asNumber() {
2333
- try {
2334
- return parseFloat(this.value);
2335
- } catch (error) {
2336
- return 0;
2337
- }
2338
- }
2339
- asString() {
2340
- return this.value;
2341
- }
2342
- toString() {
2343
- return this.asString();
2344
- }
2251
+ var ARG_KIND_OVERRIDES = {
2252
+ // apiCall(method, path, jsonBody?, jsonHeaders?) — body/headers must be
2253
+ // JSON strings; replaces the old local E4 check.
2254
+ apiCall: { 2: "jsonObjectString", 3: "jsonObjectString" }
2345
2255
  };
2346
-
2347
- // vendor/dsl/parser/lib/user-define-function.ts
2348
- var UserDefineFunction = class {
2349
- static {
2350
- __name(this, "UserDefineFunction");
2351
- }
2352
- argNames;
2353
- body;
2354
- constructor(argNames, body) {
2355
- this.argNames = argNames;
2356
- this.body = body;
2357
- }
2358
- getArgsCount() {
2359
- return this.argNames.length;
2360
- }
2361
- getArgsName(index) {
2362
- if (index < 0 || index >= this.argNames.length) {
2363
- return "";
2364
- }
2365
- return this.argNames[index];
2366
- }
2367
- execute(...args) {
2368
- try {
2369
- this.body.execute();
2370
- return NumberValue.ZERO;
2371
- } catch (e) {
2372
- if (e instanceof ReturnStatement) {
2373
- return e.getResult();
2374
- }
2375
- throw e;
2376
- }
2377
- }
2256
+ var RETURN_KIND = {
2257
+ void: "void",
2258
+ string: "string",
2259
+ number: "number",
2260
+ boolean: "bool",
2261
+ selector: "locator",
2262
+ any: "unknown"
2378
2263
  };
2379
-
2380
- // vendor/dsl/parser/lib/variables.ts
2381
- var Variables = class {
2264
+ function contractFor(fn) {
2265
+ const required = fn.minArgs ?? fn.argTypes.length;
2266
+ const overrides = ARG_KIND_OVERRIDES[fn.name] ?? {};
2267
+ return {
2268
+ name: fn.name,
2269
+ signature: {
2270
+ args: fn.argTypes.map((t, i) => ({
2271
+ label: `arg${i + 1}`,
2272
+ kind: overrides[i] ?? ARG_KIND[t],
2273
+ required: i < required
2274
+ })),
2275
+ ...fn.variadic ? { variadic: true } : {}
2276
+ },
2277
+ support: "visual",
2278
+ returns: { mode: "fixed", kind: RETURN_KIND[fn.returnType] },
2279
+ // Mobile's frozen subset (D-4) forbids method chains entirely — the
2280
+ // slim linter rejects MemberCallExpression before the engine ever
2281
+ // sees a chain, so no contract is chainable.
2282
+ locatorCapability: fn.returnType === "selector" ? "locator" : "none"
2283
+ };
2284
+ }
2285
+ __name(contractFor, "contractFor");
2286
+ function helperContract(name) {
2287
+ return {
2288
+ name,
2289
+ signature: { args: [], variadic: true },
2290
+ support: "visual",
2291
+ returns: { mode: "fixed", kind: "unknown" },
2292
+ locatorCapability: "none",
2293
+ trusted: true
2294
+ };
2295
+ }
2296
+ __name(helperContract, "helperContract");
2297
+ var MobileDslRegistry = class {
2382
2298
  static {
2383
- __name(this, "Variables");
2384
- }
2385
- static stack = [];
2386
- static variables = /* @__PURE__ */ new Map([
2387
- ["PI", new NumberValue(Math.PI)],
2388
- ["E", new NumberValue(Math.E)]
2389
- ]);
2390
- static push() {
2391
- const clone = /* @__PURE__ */ new Map([]);
2392
- for (const [key, value] of this.variables.entries()) {
2393
- clone.set(key, value);
2394
- }
2395
- this.stack.push(clone);
2299
+ __name(this, "MobileDslRegistry");
2396
2300
  }
2397
- static pop() {
2398
- const v = this.stack.pop();
2399
- if (!v) {
2400
- throw new Error("Variables stack undefined");
2301
+ byName = /* @__PURE__ */ new Map();
2302
+ constructor(functions, userFunctionNames = []) {
2303
+ for (const fn of functions) this.byName.set(fn.name, contractFor(fn));
2304
+ for (const name of userFunctionNames) {
2305
+ if (!this.byName.has(name)) this.byName.set(name, helperContract(name));
2401
2306
  }
2402
- this.variables = v;
2403
2307
  }
2404
- static isExists(key) {
2405
- return this.variables.has(key);
2308
+ resolve(name) {
2309
+ return this.byName.get(name) ?? null;
2406
2310
  }
2407
- static get(key) {
2408
- return this.variables.get(key) || NumberValue.ZERO;
2311
+ resolveContract(name) {
2312
+ return this.resolve(name);
2409
2313
  }
2410
- static set(name, value) {
2411
- this.variables.set(name, value);
2314
+ inferReturnKind(expression, _session) {
2315
+ const contract = this.resolveContract(expression.name);
2316
+ if (!contract || contract.returns.mode !== "fixed") return "unknown";
2317
+ if (contract.returns.kind === "void") return "unknown";
2318
+ return contract.returns.kind;
2412
2319
  }
2413
2320
  };
2321
+ var MOBILE_DISABLED_ENGINE_RULES = [
2322
+ "unsupported-statement",
2323
+ "unsupported-expression",
2324
+ "for-shape",
2325
+ "var-shape",
2326
+ "if-shape",
2327
+ "string-concat",
2328
+ "semantic-loss",
2329
+ "list-arg",
2330
+ "wrapped-format"
2331
+ ];
2414
2332
 
2415
- // vendor/dsl/parser/ast/array-access-expression.ts
2416
- var ArrayAccessExpression = class {
2333
+ // src/dsl/function-registry.ts
2334
+ var FunctionRegistry = class {
2417
2335
  static {
2418
- __name(this, "ArrayAccessExpression");
2419
- }
2420
- variable;
2421
- indexes;
2422
- token;
2423
- constructor(variable, indexes, token) {
2424
- this.variable = variable;
2425
- this.indexes = indexes;
2426
- this.token = token;
2427
- }
2428
- eval() {
2429
- const i = this.lastIndex();
2430
- const arr = this.getArray();
2431
- return arr.get(i);
2432
- }
2433
- lastIndex() {
2434
- return this.index(this.indexes.length - 1);
2435
- }
2436
- index(index) {
2437
- return this.indexes[index].eval().asNumber();
2438
- }
2439
- getArray() {
2440
- let arr = this.consumeArray(Variables.get(this.variable));
2441
- const last = this.indexes.length - 1;
2442
- for (let i = 0; i < last; i++) {
2443
- arr = this.consumeArray(arr.get(this.index(i)));
2444
- }
2445
- return arr;
2336
+ __name(this, "FunctionRegistry");
2446
2337
  }
2447
- consumeArray(value) {
2448
- if (value instanceof ArrayValue) {
2449
- return value;
2338
+ map = /* @__PURE__ */ new Map();
2339
+ register(fn) {
2340
+ if (this.map.has(fn.name)) {
2341
+ throw new Error(`Duplicate DSL function: ${fn.name}`);
2450
2342
  }
2451
- throw new Error("Array expected");
2452
- }
2453
- toString() {
2454
- return `${this.variable + this.indexes}`;
2455
- }
2456
- };
2457
-
2458
- // vendor/dsl/parser/ast/array-assignment-statement.ts
2459
- var ArrayAssignmentStatement = class {
2460
- static {
2461
- __name(this, "ArrayAssignmentStatement");
2343
+ this.map.set(fn.name, fn);
2462
2344
  }
2463
- array;
2464
- expression;
2465
- token;
2466
- constructor(array, expression, token) {
2467
- this.array = array;
2468
- this.expression = expression;
2469
- this.token = token;
2345
+ has(name) {
2346
+ return this.map.has(name);
2470
2347
  }
2471
- execute() {
2472
- this.array.getArray().set(this.array.lastIndex(), this.expression.eval());
2348
+ get(name) {
2349
+ return this.map.get(name);
2473
2350
  }
2474
- toString() {
2475
- return `${this.array} = ${this.expression}`;
2351
+ names() {
2352
+ return [...this.map.keys()].sort();
2476
2353
  }
2477
2354
  };
2478
2355
 
2479
- // vendor/dsl/parser/ast/array-expression.ts
2480
- var ArrayExpression = class {
2481
- static {
2482
- __name(this, "ArrayExpression");
2483
- }
2484
- elements;
2485
- token;
2486
- constructor(argument, token) {
2487
- this.elements = argument;
2488
- this.token = token;
2489
- }
2490
- eval() {
2491
- const values = this.elements.map((el) => el.eval());
2492
- return new ArrayValue(values);
2493
- }
2494
- toString() {
2495
- return `${this.elements.toString()}`;
2496
- }
2356
+ // src/dsl/functions/alerts.ts
2357
+ function asString(x, fn, idx) {
2358
+ if (typeof x !== "string") throw new Error(`${fn}(): arg ${idx} must be a string. Got ${typeof x}.`);
2359
+ return x;
2360
+ }
2361
+ __name(asString, "asString");
2362
+ var acceptAlert = {
2363
+ name: "acceptAlert",
2364
+ argTypes: ["string"],
2365
+ returnType: "void",
2366
+ minArgs: 0,
2367
+ invoke: /* @__PURE__ */ __name(async (runtime, buttonArg) => {
2368
+ const button = buttonArg !== void 0 ? asString(buttonArg, "acceptAlert", 0) : void 0;
2369
+ await runtime.driver.acceptAlert(runtime.currentDeviceSlot, button);
2370
+ }, "invoke")
2497
2371
  };
2498
-
2499
- // vendor/dsl/parser/ast/assignment-statement.ts
2500
- var AssignmentStatement = class {
2501
- static {
2502
- __name(this, "AssignmentStatement");
2503
- }
2504
- variable;
2505
- expression;
2506
- token;
2507
- constructor(variable, expression, token) {
2508
- this.variable = variable;
2509
- this.expression = expression;
2510
- this.token = token;
2511
- }
2512
- execute() {
2513
- const result = this.expression.eval();
2514
- Variables.set(this.variable, result);
2515
- }
2516
- toString() {
2517
- return `${this.variable} = ${this.expression}`;
2518
- }
2372
+ var dismissAlert = {
2373
+ name: "dismissAlert",
2374
+ argTypes: [],
2375
+ returnType: "void",
2376
+ invoke: /* @__PURE__ */ __name(async (runtime) => {
2377
+ await runtime.driver.dismissAlert(runtime.currentDeviceSlot);
2378
+ }, "invoke")
2519
2379
  };
2520
-
2521
- // vendor/dsl/parser/ast/binary-expression.ts
2522
- var BinaryExpression = class {
2523
- static {
2524
- __name(this, "BinaryExpression");
2525
- }
2526
- expr1;
2527
- expr2;
2528
- operation;
2529
- token;
2530
- constructor(operator, expr1, expr2, token) {
2531
- this.expr1 = expr1;
2532
- this.expr2 = expr2;
2533
- this.operation = operator;
2534
- this.token = token;
2535
- }
2536
- eval() {
2537
- const value1 = this.expr1.eval();
2538
- if (value1 instanceof StringValue || value1 instanceof ArrayValue) {
2539
- const string1 = value1.asString();
2540
- const string2 = this.expr2.eval().asString();
2541
- switch (this.operation) {
2542
- case "+":
2543
- return new StringValue(string1 + string2);
2544
- default:
2545
- throw new Error("Unrecognized operator for string value");
2546
- }
2547
- }
2548
- const number1 = this.expr1.eval().asNumber();
2549
- const number2 = this.expr2.eval().asNumber();
2550
- switch (this.operation) {
2551
- case "-":
2552
- return new NumberValue(number1 - number2);
2553
- case "+":
2554
- return new NumberValue(number1 + number2);
2555
- case "/":
2556
- return new NumberValue(number1 / number2);
2557
- case "*":
2558
- default:
2559
- return new NumberValue(number1 * number2);
2560
- }
2561
- }
2562
- toString() {
2563
- return `${this.expr1} ${this.operation} ${this.expr2}`;
2564
- }
2380
+ var readAlert = {
2381
+ name: "readAlert",
2382
+ argTypes: [],
2383
+ returnType: "string",
2384
+ invoke: /* @__PURE__ */ __name(async (runtime) => {
2385
+ const { text } = await runtime.driver.readAlert(runtime.currentDeviceSlot);
2386
+ return text;
2387
+ }, "invoke")
2565
2388
  };
2389
+ var ALERT_FUNCTIONS = [acceptAlert, dismissAlert, readAlert];
2566
2390
 
2567
- // vendor/dsl/parser/ast/block-statement.ts
2568
- var BlockStatement = class {
2569
- static {
2570
- __name(this, "BlockStatement");
2571
- }
2572
- statements = [];
2573
- token;
2574
- constructor(token) {
2575
- this.token = token;
2576
- }
2577
- add(statement) {
2578
- this.statements.push(statement);
2391
+ // src/dsl/functions/asserts.ts
2392
+ function asSelector(x, fn, idx) {
2393
+ if (typeof x !== "object" || x === null) {
2394
+ throw new Error(`${fn}(): arg ${idx} must be a Selector. Got ${typeof x}.`);
2579
2395
  }
2580
- execute() {
2581
- for (const statement of this.statements) {
2582
- statement.execute();
2396
+ return x;
2397
+ }
2398
+ __name(asSelector, "asSelector");
2399
+ function asNumber(x, fn, idx) {
2400
+ if (typeof x !== "number") throw new Error(`${fn}(): arg ${idx} must be a number. Got ${typeof x}.`);
2401
+ return x;
2402
+ }
2403
+ __name(asNumber, "asNumber");
2404
+ var assertEqual = {
2405
+ name: "assertEqual",
2406
+ argTypes: ["any", "any"],
2407
+ returnType: "void",
2408
+ invoke: /* @__PURE__ */ __name((_runtime, actual, expected) => {
2409
+ if (actual !== expected) {
2410
+ throw new Error(
2411
+ `assertEqual failed: actual ${JSON.stringify(actual)} !== expected ${JSON.stringify(expected)}`
2412
+ );
2583
2413
  }
2584
- }
2585
- toString() {
2586
- return `${this.statements.join("\n")}`;
2587
- }
2414
+ }, "invoke")
2588
2415
  };
2589
-
2590
- // vendor/dsl/parser/ast/break-statement.ts
2591
- var BreakStatement = class extends Error {
2592
- static {
2593
- __name(this, "BreakStatement");
2594
- }
2595
- token;
2596
- constructor(token) {
2597
- super("BreakStatement");
2598
- this.token = token;
2599
- }
2600
- execute() {
2601
- throw this;
2602
- }
2603
- toString() {
2604
- return `break`;
2605
- }
2416
+ var assertVisible = {
2417
+ name: "assertVisible",
2418
+ argTypes: ["selector"],
2419
+ returnType: "void",
2420
+ invoke: /* @__PURE__ */ __name(async (runtime, selectorArg) => {
2421
+ const selector = asSelector(selectorArg, "assertVisible", 0);
2422
+ const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
2423
+ const r = runtime.selectorResolver.resolve(tree, selector);
2424
+ if (!r.ok) {
2425
+ throw new Error(
2426
+ `assertVisible failed: ${r.reason}
2427
+ selector: ${JSON.stringify(selector)}
2428
+ candidates: ${JSON.stringify(r.candidates).slice(0, 600)}`
2429
+ );
2430
+ }
2431
+ }, "invoke")
2606
2432
  };
2607
-
2608
- // vendor/dsl/parser/ast/conditional-expression.ts
2609
- var ConditionalExpression = class {
2610
- static {
2611
- __name(this, "ConditionalExpression");
2612
- }
2613
- expr1;
2614
- expr2;
2615
- operation;
2616
- token;
2617
- constructor(operator, expr1, expr2, token) {
2618
- this.expr1 = expr1;
2619
- this.expr2 = expr2;
2620
- this.operation = operator;
2621
- this.token = token;
2622
- }
2623
- eval() {
2624
- const value1 = this.expr1.eval();
2625
- const value2 = this.expr2.eval();
2626
- let number1;
2627
- let number2;
2628
- if (value1 instanceof StringValue) {
2629
- number1 = number1 = value1.asString().localeCompare(value2.asString());
2630
- number2 = 0;
2631
- } else {
2632
- number1 = this.expr1.eval().asNumber();
2633
- number2 = this.expr2.eval().asNumber();
2433
+ var assertCount = {
2434
+ name: "assertCount",
2435
+ argTypes: ["selector", "number"],
2436
+ returnType: "void",
2437
+ invoke: /* @__PURE__ */ __name(async (runtime, selectorArg, expectedArg) => {
2438
+ const selector = asSelector(selectorArg, "assertCount", 0);
2439
+ const expected = asNumber(expectedArg, "assertCount", 1);
2440
+ const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
2441
+ let count = 0;
2442
+ while (true) {
2443
+ const probe = { ...selector, ordinal: count };
2444
+ const r = runtime.selectorResolver.resolve(tree, probe);
2445
+ if (!r.ok) break;
2446
+ count++;
2447
+ if (count > 1e3) {
2448
+ throw new Error(`assertCount: aborting after 1000 \u2014 selector too broad`);
2449
+ }
2634
2450
  }
2635
- let result;
2636
- switch (this.operation) {
2637
- case "<" /* LT */:
2638
- result = number1 < number2;
2639
- break;
2640
- case "<=" /* LTE */:
2641
- result = number1 <= number2;
2642
- break;
2643
- case ">" /* GT */:
2644
- result = number1 > number2;
2645
- break;
2646
- case ">=" /* GTE */:
2647
- result = number1 >= number2;
2648
- break;
2649
- case "!=" /* NEQ */:
2650
- result = number1 != number2;
2651
- break;
2652
- case "&&" /* AND */:
2653
- result = number1 != 0 && number2 != 0;
2654
- break;
2655
- case "||" /* OR */:
2656
- result = number1 != 0 || number2 != 0;
2657
- break;
2658
- case "==" /* EQ */:
2659
- default:
2660
- result = number1 === number2;
2451
+ if (count !== expected) {
2452
+ throw new Error(
2453
+ `assertCount failed: ${JSON.stringify(selector)} matched ${count} time(s), expected ${expected}`
2454
+ );
2661
2455
  }
2662
- return new NumberValue(result);
2663
- }
2664
- toString() {
2665
- return `${this.expr1} ${this.operation} ${this.expr2}`;
2666
- }
2456
+ }, "invoke")
2667
2457
  };
2668
-
2669
- // vendor/dsl/parser/ast/continue-statement.ts
2670
- var ContinueStatement = class extends Error {
2671
- static {
2672
- __name(this, "ContinueStatement");
2673
- }
2674
- token;
2675
- constructor(token) {
2676
- super("ContinueStatement");
2677
- this.token = token;
2458
+ async function assertEnabledState(runtime, selectorArg, expected, fnName) {
2459
+ const selector = asSelector(selectorArg, fnName, 0);
2460
+ const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
2461
+ const r = runtime.selectorResolver.resolve(tree, selector);
2462
+ if (!r.ok) {
2463
+ throw new Error(
2464
+ `${fnName} failed: selector did not match. ${r.reason}
2465
+ selector: ${JSON.stringify(selector)}`
2466
+ );
2678
2467
  }
2679
- execute() {
2680
- throw this;
2681
- }
2682
- toString() {
2683
- return `continue`;
2684
- }
2685
- };
2686
-
2687
- // vendor/dsl/parser/ast/do-while-statement.ts
2688
- var DoWhileStatement = class {
2689
- static {
2690
- __name(this, "DoWhileStatement");
2691
- }
2692
- condition;
2693
- statement;
2694
- token;
2695
- constructor(expression, statement, token) {
2696
- this.condition = expression;
2697
- this.statement = statement;
2698
- this.token = token;
2699
- }
2700
- execute() {
2701
- do {
2702
- try {
2703
- this.statement.execute();
2704
- } catch (e) {
2705
- if (e instanceof BreakStatement) {
2706
- break;
2707
- } else if (e instanceof ContinueStatement) {
2708
- } else {
2709
- throw e;
2710
- }
2711
- }
2712
- } while (this.condition.eval().asNumber() != 0);
2713
- }
2714
- toString() {
2715
- return `do {
2716
- ${this.statement}
2717
- }
2718
- while (${this.condition})
2719
- `;
2720
- }
2721
- };
2722
-
2723
- // vendor/dsl/parser/ast/for-statement.ts
2724
- var ForStatement = class {
2725
- static {
2726
- __name(this, "ForStatement");
2727
- }
2728
- initialization;
2729
- termination;
2730
- increment;
2731
- block;
2732
- token;
2733
- constructor(initialization, termination, increment, block, token) {
2734
- this.initialization = initialization;
2735
- this.termination = termination;
2736
- this.increment = increment;
2737
- this.block = block;
2738
- this.token = token;
2739
- }
2740
- execute() {
2741
- for (this.initialization.execute(); this.termination.eval().asNumber() != 0; this.increment.execute()) {
2742
- try {
2743
- this.block.execute();
2744
- } catch (e) {
2745
- if (e instanceof BreakStatement) {
2746
- break;
2747
- } else if (e instanceof ContinueStatement) {
2748
- } else {
2749
- throw e;
2750
- }
2751
- }
2752
- }
2753
- }
2754
- toString() {
2755
- return `for (${this.initialization}; ${this.termination}; ${this.increment})
2756
- {
2757
- ${this.block}
2758
- }`;
2759
- }
2760
- };
2761
-
2762
- // vendor/dsl/parser/ast/function-define-statement.ts
2763
- var FunctionDefineStatement = class {
2764
- static {
2765
- __name(this, "FunctionDefineStatement");
2766
- }
2767
- name;
2768
- argNames;
2769
- body;
2770
- token;
2771
- constructor(name, argNames, body, token) {
2772
- this.name = name;
2773
- this.argNames = argNames;
2774
- this.body = body;
2775
- this.token = token;
2776
- }
2777
- execute() {
2778
- Functions.set(this.name, new UserDefineFunction(this.argNames, this.body));
2779
- }
2780
- toString() {
2781
- const argsString = this.argNames.map((arg) => arg.toString()).join(", ");
2782
- return `function ${this.name}(${argsString})`;
2783
- }
2784
- };
2785
-
2786
- // vendor/dsl/parser/ast/function-statement.ts
2787
- var FunctionStatement = class {
2788
- static {
2789
- __name(this, "FunctionStatement");
2790
- }
2791
- functionalExpression;
2792
- token;
2793
- constructor(func, token) {
2794
- this.functionalExpression = func;
2795
- this.token = token;
2796
- }
2797
- execute() {
2798
- this.functionalExpression.eval();
2799
- }
2800
- toString() {
2801
- return `${this.functionalExpression.toString()}`;
2802
- }
2803
- };
2804
-
2805
- // vendor/dsl/parser/ast/functional-expression.ts
2806
- var FunctionalExpression = class {
2807
- static {
2808
- __name(this, "FunctionalExpression");
2809
- }
2810
- name;
2811
- arguments;
2812
- token;
2813
- constructor(name, argument, token) {
2814
- this.name = name;
2815
- this.arguments = argument;
2816
- this.token = token;
2817
- }
2818
- addArgument(argument) {
2819
- return this.arguments.push(argument);
2820
- }
2821
- eval() {
2822
- const values = this.arguments.map((v) => v.eval());
2823
- const size = values.length;
2824
- const func = Functions.get(this.name);
2825
- if (func instanceof UserDefineFunction) {
2826
- const userFunction = func;
2827
- if (values.length !== userFunction.getArgsCount()) {
2828
- throw new Error(`Invalid argument count for function ${this.name}: expected ${size}`);
2829
- }
2830
- Variables.push();
2831
- for (let i = 0; i < size; i++) {
2832
- Variables.set(userFunction.getArgsName(i), values[i]);
2833
- }
2834
- const result = userFunction.execute(...values);
2835
- Variables.pop();
2836
- return result;
2837
- }
2838
- return func.execute(...values);
2839
- }
2840
- toString() {
2841
- const argsString = this.arguments.map((arg) => arg.toString()).join(", ");
2842
- return `${this.name}(${argsString})`;
2843
- }
2844
- };
2845
-
2846
- // vendor/dsl/parser/ast/if-statement.ts
2847
- var IfStatement = class {
2848
- static {
2849
- __name(this, "IfStatement");
2850
- }
2851
- expression;
2852
- ifStatement;
2853
- elseStatement;
2854
- token;
2855
- constructor(expression, ifStatement, elseStatement, token) {
2856
- this.expression = expression;
2857
- this.ifStatement = ifStatement;
2858
- this.elseStatement = elseStatement;
2859
- this.token = token;
2860
- }
2861
- execute() {
2862
- const result = this.expression.eval().asNumber();
2863
- if (result != 0) {
2864
- this.ifStatement.execute();
2865
- } else if (this.elseStatement != null) {
2866
- this.elseStatement.execute();
2867
- }
2868
- }
2869
- toString() {
2870
- let result = `if ${this.expression} ${this.ifStatement}`;
2871
- if (this.elseStatement != null) {
2872
- result += ` else ${this.elseStatement}`;
2873
- }
2874
- return result.toString();
2875
- }
2876
- };
2877
-
2878
- // vendor/dsl/parser/ast/increment-expression.ts
2879
- var IncrementExpression = class {
2880
- static {
2881
- __name(this, "IncrementExpression");
2882
- }
2883
- variable;
2884
- type;
2885
- token;
2886
- constructor(variable, type, token) {
2887
- this.variable = variable;
2888
- this.type = type;
2889
- this.token = token;
2890
- }
2891
- eval() {
2892
- const varName = this.variable.getName();
2893
- const currentValue = Variables.get(varName);
2894
- if (!(currentValue instanceof NumberValue)) {
2895
- throw new Error(`Cannot increment/decrement non-numeric value`);
2896
- }
2897
- const currentNumber = currentValue.asNumber();
2898
- let newNumber;
2899
- switch (this.type) {
2900
- case 0 /* PREFIX_INCREMENT */:
2901
- newNumber = currentNumber + 1;
2902
- Variables.set(varName, new NumberValue(newNumber));
2903
- return new NumberValue(newNumber);
2904
- case 1 /* PREFIX_DECREMENT */:
2905
- newNumber = currentNumber - 1;
2906
- Variables.set(varName, new NumberValue(newNumber));
2907
- return new NumberValue(newNumber);
2908
- case 2 /* POSTFIX_INCREMENT */:
2909
- Variables.set(varName, new NumberValue(currentNumber + 1));
2910
- return new NumberValue(currentNumber);
2911
- case 3 /* POSTFIX_DECREMENT */:
2912
- Variables.set(varName, new NumberValue(currentNumber - 1));
2913
- return new NumberValue(currentNumber);
2914
- }
2915
- }
2916
- };
2917
-
2918
- // vendor/dsl/parser/ast/increment-statement.ts
2919
- var IncrementStatement = class {
2920
- static {
2921
- __name(this, "IncrementStatement");
2922
- }
2923
- expression;
2924
- token;
2925
- constructor(expression, token) {
2926
- this.expression = expression;
2927
- this.token = token;
2928
- }
2929
- execute() {
2930
- this.expression.eval();
2931
- }
2932
- };
2933
-
2934
- // vendor/dsl/parser/ast/meta-block-statement.ts
2935
- var MetaBlockStatement = class {
2936
- static {
2937
- __name(this, "MetaBlockStatement");
2938
- }
2939
- name;
2940
- argNames;
2941
- body;
2942
- token;
2943
- constructor(name, argNames, body, token) {
2944
- this.name = name;
2945
- this.argNames = argNames;
2946
- this.body = body;
2947
- this.token = token;
2948
- }
2949
- execute() {
2950
- }
2951
- toString() {
2952
- const argsString = this.argNames.map((arg) => arg.toString()).join(", ");
2953
- return `//@${this.name}(${argsString})
2954
- ${this.body}
2955
- //@end-${this.name}`;
2956
- }
2957
- };
2958
-
2959
- // vendor/dsl/parser/ast/object-expression.ts
2960
- var ObjectExpression = class {
2961
- static {
2962
- __name(this, "ObjectExpression");
2963
- }
2964
- properties;
2965
- token;
2966
- constructor(properties = /* @__PURE__ */ new Map(), token) {
2967
- this.properties = properties;
2968
- this.token = token;
2969
- }
2970
- eval() {
2971
- const evaluatedProperties = /* @__PURE__ */ new Map();
2972
- this.properties.forEach((expression, key) => {
2973
- evaluatedProperties.set(key, expression.eval());
2974
- });
2975
- return new ObjectValue(evaluatedProperties);
2976
- }
2977
- toString() {
2978
- const entries = Array.from(this.properties.entries()).map(([key, value]) => `${key}: ${value.toString()}`).join(", ");
2979
- return `{${entries}}`;
2980
- }
2981
- };
2982
-
2983
- // vendor/dsl/parser/ast/print-statement.ts
2984
- var PrintStatement = class {
2985
- static {
2986
- __name(this, "PrintStatement");
2987
- }
2988
- expression;
2989
- token;
2990
- constructor(expression, token) {
2991
- this.expression = expression;
2992
- this.token = token;
2993
- }
2994
- execute() {
2995
- const result = this.expression.eval().asString();
2996
- console.log(`${result}`);
2997
- }
2998
- toString() {
2999
- return `print ${this.expression}`;
3000
- }
3001
- };
3002
-
3003
- // vendor/dsl/parser/ast/return-statement.ts
3004
- var ReturnStatement = class extends Error {
3005
- static {
3006
- __name(this, "ReturnStatement");
3007
- }
3008
- expression;
3009
- result;
3010
- token;
3011
- constructor(expression, token) {
3012
- super("ContinueStatement");
3013
- this.expression = expression;
3014
- this.token = token;
3015
- }
3016
- getResult() {
3017
- if (!this.result) {
3018
- throw new Error("Not execute ReturnStatement");
3019
- }
3020
- return this.result;
3021
- }
3022
- execute() {
3023
- this.result = this.expression.eval();
3024
- throw this;
3025
- }
3026
- toString() {
3027
- return `return`;
3028
- }
3029
- };
3030
-
3031
- // vendor/dsl/parser/ast/unary-expression.ts
3032
- var UnaryExpression = class {
3033
- static {
3034
- __name(this, "UnaryExpression");
3035
- }
3036
- expr1;
3037
- operation;
3038
- token;
3039
- constructor(operator, expr1, token) {
3040
- this.operation = operator;
3041
- this.expr1 = expr1;
3042
- this.token = token;
3043
- }
3044
- eval() {
3045
- switch (this.operation) {
3046
- case "-":
3047
- return new NumberValue(-this.expr1.eval().asNumber());
3048
- case "+":
3049
- default:
3050
- return new NumberValue(this.expr1.eval().asNumber());
3051
- }
3052
- }
3053
- toString() {
3054
- return `${this.operation}${this.expr1}`;
3055
- }
3056
- };
3057
-
3058
- // vendor/dsl/parser/token-type.ts
3059
- var TokenType = /* @__PURE__ */ ((TokenType2) => {
3060
- TokenType2[TokenType2["NUMBER"] = 0] = "NUMBER";
3061
- TokenType2[TokenType2["WORD"] = 1] = "WORD";
3062
- TokenType2[TokenType2["TEXT"] = 2] = "TEXT";
3063
- TokenType2[TokenType2["VAR"] = 3] = "VAR";
3064
- TokenType2[TokenType2["PRINT"] = 4] = "PRINT";
3065
- TokenType2[TokenType2["IF"] = 5] = "IF";
3066
- TokenType2[TokenType2["ELSE"] = 6] = "ELSE";
3067
- TokenType2[TokenType2["WHILE"] = 7] = "WHILE";
3068
- TokenType2[TokenType2["FOR"] = 8] = "FOR";
3069
- TokenType2[TokenType2["DO"] = 9] = "DO";
3070
- TokenType2[TokenType2["BREAK"] = 10] = "BREAK";
3071
- TokenType2[TokenType2["CONTINUE"] = 11] = "CONTINUE";
3072
- TokenType2[TokenType2["FUNCTION"] = 12] = "FUNCTION";
3073
- TokenType2[TokenType2["RETURN"] = 13] = "RETURN";
3074
- TokenType2[TokenType2["PLUS"] = 14] = "PLUS";
3075
- TokenType2[TokenType2["PLUSPLUS"] = 15] = "PLUSPLUS";
3076
- TokenType2[TokenType2["MINUS"] = 16] = "MINUS";
3077
- TokenType2[TokenType2["MINUSMINUS"] = 17] = "MINUSMINUS";
3078
- TokenType2[TokenType2["STAR"] = 18] = "STAR";
3079
- TokenType2[TokenType2["SLASH"] = 19] = "SLASH";
3080
- TokenType2[TokenType2["EQ"] = 20] = "EQ";
3081
- TokenType2[TokenType2["EQEQ"] = 21] = "EQEQ";
3082
- TokenType2[TokenType2["EXCL"] = 22] = "EXCL";
3083
- TokenType2[TokenType2["EXCLEQ"] = 23] = "EXCLEQ";
3084
- TokenType2[TokenType2["LT"] = 24] = "LT";
3085
- TokenType2[TokenType2["LTEQ"] = 25] = "LTEQ";
3086
- TokenType2[TokenType2["GT"] = 26] = "GT";
3087
- TokenType2[TokenType2["GTEQ"] = 27] = "GTEQ";
3088
- TokenType2[TokenType2["BAR"] = 28] = "BAR";
3089
- TokenType2[TokenType2["BARBAR"] = 29] = "BARBAR";
3090
- TokenType2[TokenType2["AMP"] = 30] = "AMP";
3091
- TokenType2[TokenType2["AMPAMP"] = 31] = "AMPAMP";
3092
- TokenType2[TokenType2["LPAREN"] = 32] = "LPAREN";
3093
- TokenType2[TokenType2["RPAREN"] = 33] = "RPAREN";
3094
- TokenType2[TokenType2["LBRACE"] = 34] = "LBRACE";
3095
- TokenType2[TokenType2["RBRACE"] = 35] = "RBRACE";
3096
- TokenType2[TokenType2["LBRACKET"] = 36] = "LBRACKET";
3097
- TokenType2[TokenType2["RBRACKET"] = 37] = "RBRACKET";
3098
- TokenType2[TokenType2["COMMA"] = 38] = "COMMA";
3099
- TokenType2[TokenType2["COLON"] = 39] = "COLON";
3100
- TokenType2[TokenType2["SEMICOLON"] = 40] = "SEMICOLON";
3101
- TokenType2[TokenType2["META_BLOCK"] = 41] = "META_BLOCK";
3102
- TokenType2[TokenType2["CLOSE_META_BLOCK"] = 42] = "CLOSE_META_BLOCK";
3103
- TokenType2[TokenType2["EOF"] = 43] = "EOF";
3104
- return TokenType2;
3105
- })(TokenType || {});
3106
-
3107
- // vendor/dsl/parser/ast/value-expression.ts
3108
- var ValueExpression = class {
3109
- static {
3110
- __name(this, "ValueExpression");
3111
- }
3112
- value;
3113
- token;
3114
- constructor(value, token) {
3115
- this.token = token;
3116
- if (token.getType() === 0 /* NUMBER */) {
3117
- this.value = new NumberValue(parseFloat(value));
3118
- return;
3119
- }
3120
- if (token.getType() === 2 /* TEXT */) {
3121
- this.value = new StringValue(value);
3122
- return;
3123
- }
3124
- throw new Error(`Unrecognized value. ${token}`);
3125
- }
3126
- eval() {
3127
- return this.value;
3128
- }
3129
- toString() {
3130
- return `${this.value.asString()}`;
3131
- }
3132
- };
3133
-
3134
- // vendor/dsl/parser/lib/undefined-value.ts
3135
- var UndefinedValue = class _UndefinedValue {
3136
- static {
3137
- __name(this, "UndefinedValue");
3138
- }
3139
- static UNDEFINED = new _UndefinedValue();
3140
- clone() {
3141
- return _UndefinedValue.UNDEFINED;
3142
- }
3143
- asNumber() {
3144
- return NaN;
3145
- }
3146
- asString() {
3147
- return "undefined";
3148
- }
3149
- toString() {
3150
- return this.asString();
3151
- }
3152
- };
3153
-
3154
- // vendor/dsl/parser/ast/var-statement.ts
3155
- var VarStatement = class {
3156
- static {
3157
- __name(this, "VarStatement");
3158
- }
3159
- declarators;
3160
- token;
3161
- constructor(declarators, token) {
3162
- this.declarators = declarators;
3163
- this.token = token;
3164
- }
3165
- execute() {
3166
- for (const declarator of this.declarators) {
3167
- if (declarator.init) {
3168
- const value = declarator.init.eval();
3169
- Variables.set(declarator.name, value);
3170
- } else {
3171
- Variables.set(declarator.name, UndefinedValue.UNDEFINED);
3172
- }
3173
- }
3174
- }
3175
- toString() {
3176
- const decls = this.declarators.map((d) => d.init ? `${d.name} = ${d.init}` : d.name).join(", ");
3177
- return `var ${decls}`;
3178
- }
3179
- };
3180
-
3181
- // vendor/dsl/parser/ast/variable-expression.ts
3182
- var VariableExpression = class {
3183
- static {
3184
- __name(this, "VariableExpression");
3185
- }
3186
- name;
3187
- token;
3188
- constructor(name, token) {
3189
- this.name = name;
3190
- this.token = token;
3191
- }
3192
- eval() {
3193
- if (!Variables.isExists(this.name)) {
3194
- throw new Error(`Constant ${this.name} does not exist!`);
3195
- }
3196
- return Variables.get(this.name);
3197
- }
3198
- getName() {
3199
- return this.name;
3200
- }
3201
- toString() {
3202
- return `${this.name}`;
3203
- }
3204
- };
3205
-
3206
- // vendor/dsl/parser/ast/while-statement.ts
3207
- var WhileStatement = class {
3208
- static {
3209
- __name(this, "WhileStatement");
3210
- }
3211
- condition;
3212
- statement;
3213
- token;
3214
- constructor(expression, statement, token) {
3215
- this.condition = expression;
3216
- this.statement = statement;
3217
- this.token = token;
3218
- }
3219
- execute() {
3220
- while (this.condition.eval().asNumber() != 0) {
3221
- try {
3222
- this.statement.execute();
3223
- } catch (e) {
3224
- if (e instanceof BreakStatement) {
3225
- break;
3226
- } else if (e instanceof ContinueStatement) {
3227
- } else {
3228
- throw e;
3229
- }
3230
- }
3231
- }
3232
- }
3233
- toString() {
3234
- return `while (${this.condition})
3235
- {
3236
- ${this.statement}
3237
- }`;
3238
- }
3239
- };
3240
-
3241
- // src/dsl/return-signal.ts
3242
- var ReturnSignal = class extends Error {
3243
- constructor(value) {
3244
- super("ReturnSignal");
3245
- this.value = value;
3246
- this.name = "ReturnSignal";
3247
- }
3248
- value;
3249
- static {
3250
- __name(this, "ReturnSignal");
3251
- }
3252
- };
3253
-
3254
- // src/dsl/user-function-resolver.ts
3255
- var UserFunctionResolver = class {
3256
- static {
3257
- __name(this, "UserFunctionResolver");
3258
- }
3259
- fns = /* @__PURE__ */ new Map();
3260
- register(fnDef, source) {
3261
- if (this.fns.has(fnDef.name)) {
3262
- const existing = this.fns.get(fnDef.name);
3263
- const existingTok = existing.token?.getLine?.();
3264
- throw new Error(
3265
- `Duplicate user function "${fnDef.name}"` + (existingTok ? ` (already defined at line ${existingTok}${source ? ` in ${source}` : ""})` : "")
3266
- );
3267
- }
3268
- this.fns.set(fnDef.name, fnDef);
3269
- }
3270
- get(name) {
3271
- return this.fns.get(name);
3272
- }
3273
- has(name) {
3274
- return this.fns.has(name);
3275
- }
3276
- names() {
3277
- return [...this.fns.keys()].sort();
3278
- }
3279
- };
3280
-
3281
- // src/dsl/ast-executor.ts
3282
- var UnsupportedAstNodeError = class extends Error {
3283
- static {
3284
- __name(this, "UnsupportedAstNodeError");
3285
- }
3286
- constructor(nodeType, line, col) {
3287
- super(`Unsupported AST node "${nodeType}" at ${line}:${col} \u2014 not allowed by MVP DSL subset (D-4)`);
3288
- this.name = "UnsupportedAstNodeError";
3289
- }
3290
- };
3291
- var DEFAULT_MAX_CALL_DEPTH = 32;
3292
- function isBlockMappable(name) {
3293
- return name.startsWith("test_") || name.startsWith("flow_");
3294
- }
3295
- __name(isBlockMappable, "isBlockMappable");
3296
- var AstExecutor = class {
3297
- constructor(registry) {
3298
- this.registry = registry;
3299
- }
3300
- registry;
3301
- static {
3302
- __name(this, "AstExecutor");
3303
- }
3304
- /**
3305
- * Run the test function `testName` from the parsed AST `program`.
3306
- *
3307
- * @param program entry scenario AST
3308
- * @param testName name of the entry test_/flow_ function
3309
- * @param runtime per-run state container
3310
- * @param opts execution options (budgets, mode)
3311
- * @param helpers optional helper-file ASTs (Stage 2); their top-level
3312
- * FunctionDefineStatements are registered alongside
3313
- * the entry's helpers in the user-function resolver
3314
- */
3315
- async *run(program, testName, runtime, opts, helpers) {
3316
- const start = Date.now();
3317
- let stepsExecuted = 0;
3318
- const userFns = new UserFunctionResolver();
3319
- const programs = [program, ...helpers ?? []];
3320
- for (const p of programs) {
3321
- for (const stmt of p.statements) {
3322
- if (stmt instanceof FunctionDefineStatement) {
3323
- userFns.register(stmt);
3324
- }
3325
- }
3326
- }
3327
- const entry = userFns.get(testName);
3328
- if (!entry) {
3329
- const error = {
3330
- message: `Test function "${testName}" not found in scenario AST`
3331
- };
3332
- return { outcome: "failed", stepsExecuted, durationMs: Date.now() - start, error };
3333
- }
3334
- if (!(entry.body instanceof BlockStatement)) {
3335
- const error = {
3336
- message: `Test "${testName}" body is not a BlockStatement (got ${entry.body.constructor.name})`
3337
- };
3338
- return { outcome: "failed", stepsExecuted, durationMs: Date.now() - start, error };
3339
- }
3340
- if (!isBlockMappable(testName)) {
3341
- const error = {
3342
- message: `Entry function "${testName}" must start with test_ or flow_`
3343
- };
3344
- return { outcome: "failed", stepsExecuted, durationMs: Date.now() - start, error };
3345
- }
3346
- const getSteps = /* @__PURE__ */ __name(() => stepsExecuted, "getSteps");
3347
- const setSteps = /* @__PURE__ */ __name((n) => {
3348
- stepsExecuted = n;
3349
- }, "setSteps");
3350
- const callStack = [];
3351
- try {
3352
- yield* this.runBlock(entry.body, runtime, opts, getSteps, setSteps, start, userFns, callStack);
3353
- return { outcome: "completed", stepsExecuted, durationMs: Date.now() - start };
3354
- } catch (e) {
3355
- const error = toExecutionError(e);
3356
- return { outcome: "failed", stepsExecuted, durationMs: Date.now() - start, error };
3357
- }
3358
- }
3359
- // ---- internals ---------------------------------------------------------
3360
- async *runBlock(block, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack) {
3361
- for (const stmt of block.statements) {
3362
- yield* this.runStatement(stmt, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3363
- }
3364
- }
3365
- async *runStatement(stmt, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack) {
3366
- const { line, col } = locationOf(stmt);
3367
- const stackSnap = callStack.length === 0 ? void 0 : [...callStack];
3368
- yield { kind: "step-start", node: stmt, line, col, callStack: stackSnap };
3369
- let attempted = false;
3370
- while (true) {
3371
- this.checkBudgets(opts, getSteps(), runStartMs);
3372
- try {
3373
- yield* this.execStatement(stmt, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3374
- setSteps(getSteps() + 1);
3375
- yield { kind: "step-end", node: stmt, line, col, ok: true, callStack: stackSnap };
3376
- if (opts.mode === "step") yield { kind: "paused", reason: "step", line, col };
3377
- return;
3378
- } catch (e) {
3379
- if (e instanceof ReturnSignal) throw e;
3380
- const error = toExecutionError(e);
3381
- yield { kind: "step-end", node: stmt, line, col, ok: false, error, callStack: stackSnap };
3382
- if (!opts.pauseOnFailure) throw e;
3383
- yield { kind: "paused", reason: "failure", line, col };
3384
- attempted = true;
3385
- if (attempted) continue;
3386
- return;
3387
- }
3388
- }
3389
- }
3390
- async *execStatement(stmt, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack) {
3391
- if (stmt instanceof BlockStatement) {
3392
- yield* this.runBlock(stmt, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3393
- return;
3394
- }
3395
- if (stmt instanceof AssignmentStatement) {
3396
- const value = yield* this.evalExpression(stmt.expression, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3397
- runtime.setVar(stmt.variable, value);
3398
- return;
3399
- }
3400
- if (stmt instanceof FunctionStatement) {
3401
- yield* this.evalExpression(
3402
- stmt.functionalExpression,
3403
- runtime,
3404
- opts,
3405
- getSteps,
3406
- setSteps,
3407
- runStartMs,
3408
- userFns,
3409
- callStack
3410
- );
3411
- return;
3412
- }
3413
- if (stmt instanceof IfStatement) {
3414
- const cond = yield* this.evalExpression(stmt.expression, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3415
- if (isTruthy(cond)) {
3416
- yield* this.execStatement(stmt.ifStatement, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3417
- } else if (stmt.elseStatement) {
3418
- yield* this.execStatement(stmt.elseStatement, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3419
- }
3420
- return;
3421
- }
3422
- if (stmt instanceof MetaBlockStatement) {
3423
- yield* this.execStatement(stmt.body, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3424
- return;
3425
- }
3426
- if (stmt instanceof ReturnStatement) {
3427
- const expr = stmt.expression;
3428
- const value = yield* this.evalExpression(expr, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3429
- throw new ReturnSignal(value);
3430
- }
3431
- if (stmt instanceof FunctionDefineStatement) {
3432
- const { line: line2, col: col2 } = locationOf(stmt);
3433
- throw new UnsupportedAstNodeError("nested FunctionDefineStatement", line2, col2);
3434
- }
3435
- if (stmt instanceof VarStatement) {
3436
- const { line: line2, col: col2 } = locationOf(stmt);
3437
- throw new UnsupportedAstNodeError("VarStatement (use bare assignment instead)", line2, col2);
3438
- }
3439
- if (stmt instanceof ForStatement || stmt instanceof WhileStatement) {
3440
- const { line: line2, col: col2 } = locationOf(stmt);
3441
- throw new UnsupportedAstNodeError(
3442
- stmt instanceof ForStatement ? "ForStatement" : "WhileStatement",
3443
- line2,
3444
- col2
3445
- );
3446
- }
3447
- const { line, col } = locationOf(stmt);
3448
- throw new UnsupportedAstNodeError(stmt.constructor.name, line, col);
3449
- }
3450
- async *evalExpression(expr, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack) {
3451
- if (expr instanceof ValueExpression) {
3452
- const v = expr.value;
3453
- if (v instanceof StringValue) return v.asString();
3454
- if (v instanceof NumberValue) return v.asNumber();
3455
- const { line: line2, col: col2 } = locationOf(expr);
3456
- throw new UnsupportedAstNodeError(
3457
- `ValueExpression payload ${v.constructor.name}`,
3458
- line2,
3459
- col2
3460
- );
3461
- }
3462
- if (expr instanceof VariableExpression) {
3463
- if (expr.name === "true") return true;
3464
- if (expr.name === "false") return false;
3465
- if (expr.name === "null") return null;
3466
- return runtime.getVar(expr.name);
3467
- }
3468
- if (expr instanceof FunctionalExpression) {
3469
- const args = [];
3470
- for (const a of expr.arguments) {
3471
- const v = yield* this.evalExpression(a, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3472
- args.push(v);
3473
- }
3474
- const userFn = userFns.get(expr.name);
3475
- if (userFn) {
3476
- return yield* this.callUserFunction(
3477
- userFn,
3478
- args,
3479
- runtime,
3480
- opts,
3481
- getSteps,
3482
- setSteps,
3483
- runStartMs,
3484
- userFns,
3485
- callStack
3486
- );
3487
- }
3488
- const fn = this.registry.get(expr.name);
3489
- if (!fn) {
3490
- const { line: line2, col: col2 } = locationOf(expr);
3491
- throw new Error(`Unknown DSL function "${expr.name}" at ${line2}:${col2}`);
3492
- }
3493
- return await fn.invoke(runtime, ...args);
3494
- }
3495
- if (expr instanceof BinaryExpression) {
3496
- const l = yield* this.evalExpression(expr.expr1, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3497
- const r = yield* this.evalExpression(expr.expr2, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3498
- return applyBinaryOp(expr.operation, l, r);
3499
- }
3500
- if (expr instanceof ArrayExpression) {
3501
- const out = [];
3502
- for (const e of expr.elements) {
3503
- const v = yield* this.evalExpression(e, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3504
- out.push(v);
3505
- }
3506
- return out;
3507
- }
3508
- const { line, col } = locationOf(expr);
3509
- throw new UnsupportedAstNodeError(`expression ${expr.constructor.name}`, line, col);
3510
- }
3511
- /**
3512
- * Invoke a user-defined function. enterCall (maxCallDepth budget), pushScope,
3513
- * bind args, runBlock body, catch ReturnSignal → return value, popScope,
3514
- * exitCall. NOTE: does NOT increment stepsExecuted on the call itself —
3515
- * `stepsExecuted == count of emitted step-events`, invariant preserved.
3516
- */
3517
- async *callUserFunction(fnDef, args, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack) {
3518
- runtime.enterCall(opts.maxCallDepth ?? DEFAULT_MAX_CALL_DEPTH);
3519
- callStack.push(fnDef.name);
3520
- try {
3521
- runtime.pushScope();
3522
- try {
3523
- for (let i = 0; i < fnDef.argNames.length; i++) {
3524
- runtime.setVar(fnDef.argNames[i], args[i] ?? null);
3525
- }
3526
- if (!(fnDef.body instanceof BlockStatement)) {
3527
- throw new Error(`User function "${fnDef.name}" body is not a BlockStatement`);
3528
- }
3529
- try {
3530
- yield* this.runBlock(fnDef.body, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
3531
- return null;
3532
- } catch (e) {
3533
- if (e instanceof ReturnSignal) return e.value ?? null;
3534
- throw e;
3535
- }
3536
- } finally {
3537
- runtime.popScope();
3538
- }
3539
- } finally {
3540
- callStack.pop();
3541
- runtime.exitCall();
3542
- }
3543
- }
3544
- checkBudgets(opts, steps, runStartMs) {
3545
- if (opts.maxSteps !== void 0 && steps >= opts.maxSteps) {
3546
- throw new Error(`maxSteps budget (${opts.maxSteps}) exhausted`);
3547
- }
3548
- if (opts.maxDurationMs !== void 0 && Date.now() - runStartMs >= opts.maxDurationMs) {
3549
- throw new Error(`maxDurationMs budget (${opts.maxDurationMs}ms) exhausted`);
3550
- }
3551
- }
3552
- };
3553
- function locationOf(node) {
3554
- const tok = node.token;
3555
- if (!tok) return { line: 0, col: 0 };
3556
- return {
3557
- line: tok.getLine ? tok.getLine() : 0,
3558
- col: tok.getColumn ? tok.getColumn() : 0
3559
- };
3560
- }
3561
- __name(locationOf, "locationOf");
3562
- function isTruthy(v) {
3563
- if (typeof v === "number") return v !== 0;
3564
- if (typeof v === "boolean") return v;
3565
- if (typeof v === "string") return v.length > 0;
3566
- return v != null;
3567
- }
3568
- __name(isTruthy, "isTruthy");
3569
- function applyBinaryOp(op, l, r) {
3570
- const ln = typeof l === "number" ? l : Number(l);
3571
- const rn = typeof r === "number" ? r : Number(r);
3572
- switch (op) {
3573
- case "+":
3574
- if (typeof l === "string" || typeof r === "string") return String(l) + String(r);
3575
- return ln + rn;
3576
- case "-":
3577
- return ln - rn;
3578
- case "*":
3579
- return ln * rn;
3580
- case "/":
3581
- return ln / rn;
3582
- case "==":
3583
- return l === r || Number.isFinite(ln) && Number.isFinite(rn) && ln === rn;
3584
- case "!=":
3585
- return !(l === r || Number.isFinite(ln) && Number.isFinite(rn) && ln === rn);
3586
- case "<":
3587
- return ln < rn;
3588
- case "<=":
3589
- return ln <= rn;
3590
- case ">":
3591
- return ln > rn;
3592
- case ">=":
3593
- return ln >= rn;
3594
- case "&&":
3595
- return Boolean(l) && Boolean(r);
3596
- case "||":
3597
- return Boolean(l) || Boolean(r);
3598
- default:
3599
- throw new Error(`Unsupported binary operator "${op}"`);
3600
- }
3601
- }
3602
- __name(applyBinaryOp, "applyBinaryOp");
3603
- function toExecutionError(e) {
3604
- if (e instanceof Error) {
3605
- return { message: e.message, stack: e.stack ?? "" };
3606
- }
3607
- return { message: String(e) };
3608
- }
3609
- __name(toExecutionError, "toExecutionError");
3610
-
3611
- // src/dsl/linter.ts
3612
- var ASSERT_PREFIX = "assert";
3613
- var UI_CALL_NAMES = /* @__PURE__ */ new Set(["tap", "type", "swipe", "pressKey", "waitFor", "appLaunch", "openDeeplink"]);
3614
- var DslLinter = class {
3615
- constructor(deps) {
3616
- this.deps = deps;
3617
- }
3618
- deps;
3619
- static {
3620
- __name(this, "DslLinter");
3621
- }
3622
- /**
3623
- * Lint a parsed scenario AST.
3624
- *
3625
- * @param ast entry-scenario AST
3626
- * @param source raw source — when present enables E7 header check
3627
- * @param options cross-cutting context (user-function names, etc.)
3628
- */
3629
- lint(ast, source, options = {}) {
3630
- const diags = [];
3631
- const lines = source !== void 0 ? source.split("\n") : null;
3632
- const userFns = options.userFunctionNames ?? /* @__PURE__ */ new Set();
3633
- for (const stmt of ast.statements) {
3634
- if (stmt instanceof FunctionDefineStatement && isBlockMappable2(stmt.name)) {
3635
- if (lines !== null) this.checkBlocklyHeader(stmt, lines, diags);
3636
- this.lintFunctionBody(
3637
- stmt,
3638
- diags,
3639
- userFns,
3640
- /*allowReturn*/
3641
- false
3642
- );
3643
- } else if (stmt instanceof FunctionDefineStatement) {
3644
- this.lintFunctionBody(
3645
- stmt,
3646
- diags,
3647
- userFns,
3648
- /*allowReturn*/
3649
- true
3650
- );
3651
- } else if (stmt instanceof MetaBlockStatement) {
3652
- } else {
3653
- diags.push(diag("error", "E2", `Only function definitions allowed at top level (got ${stmt.constructor.name})`, stmt));
3654
- }
3655
- }
3656
- return diags;
3657
- }
3658
- /**
3659
- * Cross-file E8 check — duplicate user-function names + shadowing
3660
- * built-ins. Run ONCE per scenario load (not per file) using the
3661
- * full origin list from ScenarioLoader.userFunctionOrigins.
3662
- *
3663
- * Each diagnostic is anchored to a specific origin (with path/line/col),
3664
- * so production callers print "_helpers/seed.js:12:1 [E8] ...".
3665
- */
3666
- lintCrossFile(origins) {
3667
- const diags = [];
3668
- const byName = /* @__PURE__ */ new Map();
3669
- for (const o of origins) {
3670
- const list = byName.get(o.name) ?? [];
3671
- list.push(o);
3672
- byName.set(o.name, list);
3673
- }
3674
- for (const [name, list] of byName) {
3675
- if (this.deps.registry.has(name)) {
3676
- for (const o of list) {
3677
- diags.push({
3678
- severity: "error",
3679
- code: "E8",
3680
- message: `User function "${name}" shadows a built-in DSL function. Rename it.`,
3681
- line: o.line,
3682
- col: o.col,
3683
- source: o.source
3684
- });
3685
- }
3686
- continue;
3687
- }
3688
- if (list.length > 1) {
3689
- const where = list.map((o) => `${o.source}:${o.line}`).join(", ");
3690
- for (const o of list) {
3691
- diags.push({
3692
- severity: "error",
3693
- code: "E8",
3694
- message: `Duplicate user function "${name}". Defined at: ${where}`,
3695
- line: o.line,
3696
- col: o.col,
3697
- source: o.source
3698
- });
3699
- }
3700
- }
3701
- }
3702
- return diags;
3703
- }
3704
- /**
3705
- * Lint a helper file. Same rules as scenario, but:
3706
- * - No E7 header check (helpers aren't block-mappable)
3707
- * - `return` allowed in any function body
3708
- * - All diagnostics tagged with helper.path
3709
- */
3710
- lintHelper(helper, options = {}) {
3711
- const diags = [];
3712
- const userFns = options.userFunctionNames ?? /* @__PURE__ */ new Set();
3713
- for (const stmt of helper.ast.statements) {
3714
- if (stmt instanceof FunctionDefineStatement) {
3715
- if (isBlockMappable2(stmt.name)) {
3716
- this.lintFunctionBody(
3717
- stmt,
3718
- diags,
3719
- userFns,
3720
- /*allowReturn*/
3721
- false
3722
- );
3723
- } else {
3724
- this.lintFunctionBody(
3725
- stmt,
3726
- diags,
3727
- userFns,
3728
- /*allowReturn*/
3729
- true
3730
- );
3731
- }
3732
- } else if (stmt instanceof MetaBlockStatement) {
3733
- } else {
3734
- diags.push(diag("error", "E2", `Only function definitions allowed at top level (got ${stmt.constructor.name})`, stmt));
3735
- }
3736
- }
3737
- return diags.map((d) => ({ ...d, source: helper.path }));
3738
- }
3739
- // ---- internals --------------------------------------------------------
3740
- checkBlocklyHeader(fn, lines, diags) {
3741
- const fnLine = fn.token?.getLine?.() ?? 0;
3742
- if (fnLine <= 3) {
3743
- diags.push(diag("error", "E7", `${fn.name}: missing 3-comment Blockly header (// id-<slug>, // <name>, // #<color>) immediately above the function definition`, fn));
3744
- return;
3745
- }
3746
- const idLine = lines[fnLine - 4];
3747
- const nameLine = lines[fnLine - 3];
3748
- const colorLine = lines[fnLine - 2];
3749
- const allComments = [idLine, nameLine, colorLine].every(
3750
- (l) => typeof l === "string" && l.trimStart().startsWith("//")
3751
- );
3752
- if (!allComments) {
3753
- diags.push(diag("error", "E7", `${fn.name}: 3 comment lines must immediately precede the function (// id-<slug>, // <name>, // #<color>) \u2014 no blank line in between`, fn));
3754
- }
3755
- }
3756
- lintFunctionBody(fn, diags, userFns, allowReturn) {
3757
- if (!(fn.body instanceof BlockStatement)) {
3758
- diags.push(diag("error", "E2", `Function "${fn.name}" body must be a block`, fn));
3759
- return;
3760
- }
3761
- let firstUiSeen = false;
3762
- let firstAssertSeen = false;
3763
- const recordCall = /* @__PURE__ */ __name((name) => {
3764
- if (UI_CALL_NAMES.has(name)) firstUiSeen = true;
3765
- if (name.startsWith(ASSERT_PREFIX)) firstAssertSeen = true;
3766
- }, "recordCall");
3767
- for (const stmt of fn.body.statements) {
3768
- this.checkStatement(stmt, diags, recordCall, () => firstUiSeen, userFns, allowReturn);
3769
- }
3770
- void firstAssertSeen;
3771
- }
3772
- checkStatement(stmt, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
3773
- if (stmt instanceof ReturnStatement) {
3774
- if (!allowReturn) {
3775
- diags.push(diag("error", "E2", `'return' is not allowed inside test_*/flow_* function bodies (only in helpers)`, stmt));
3776
- return;
3777
- }
3778
- const expr = stmt.expression;
3779
- if (expr) this.checkExpression(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3780
- return;
3781
- }
3782
- if (this.isForbiddenStatement(stmt)) {
3783
- diags.push(diag("error", "E2", `Forbidden statement type "${stmt.constructor.name}" (D-4 subset)`, stmt));
3784
- return;
3785
- }
3786
- if (stmt instanceof AssignmentStatement) {
3787
- this.checkExpression(stmt.expression, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3788
- return;
3789
- }
3790
- if (stmt instanceof FunctionStatement) {
3791
- this.checkCall(stmt.functionalExpression, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3792
- return;
3793
- }
3794
- if (stmt instanceof IfStatement) {
3795
- this.checkExpression(stmt.expression, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3796
- this.checkStatement(stmt.ifStatement, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3797
- if (stmt.elseStatement) this.checkStatement(stmt.elseStatement, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3798
- return;
3799
- }
3800
- if (stmt instanceof BlockStatement) {
3801
- for (const s of stmt.statements) this.checkStatement(s, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3802
- return;
3803
- }
3804
- if (stmt instanceof FunctionDefineStatement) {
3805
- diags.push(diag("error", "E2", "Nested function definitions are not allowed", stmt));
3806
- return;
3807
- }
3808
- if (stmt instanceof MetaBlockStatement) return;
3809
- diags.push(diag("error", "E2", `Unsupported statement "${stmt.constructor.name}"`, stmt));
3810
- }
3811
- checkExpression(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
3812
- if (expr instanceof FunctionalExpression) {
3813
- this.checkCall(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3814
- return;
3815
- }
3816
- if (expr instanceof BinaryExpression) {
3817
- this.checkExpression(expr.expr1, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3818
- this.checkExpression(expr.expr2, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3819
- return;
3820
- }
3821
- if (expr instanceof ArrayExpression) {
3822
- for (const e of expr.elements) this.checkExpression(e, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3823
- return;
3824
- }
3825
- if (expr instanceof ValueExpression || expr instanceof VariableExpression) return;
3826
- if (this.isForbiddenExpression(expr)) {
3827
- diags.push(diag("error", "E2", `Forbidden expression "${expr.constructor.name}" (D-4 subset)`, expr));
3828
- return;
3829
- }
3830
- diags.push(diag("error", "E2", `Unsupported expression "${expr.constructor.name}"`, expr));
3831
- }
3832
- checkCall(call, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
3833
- const name = call.name;
3834
- if (!this.deps.registry.has(name) && !userFns.has(name)) {
3835
- diags.push(diag("error", "E1", `Unknown DSL function "${name}"`, call));
3836
- }
3837
- const fn = this.deps.registry.get(name);
3838
- if (fn) {
3839
- this.checkArity(call, fn, diags);
3840
- this.checkArgTypes(call, fn, diags);
3841
- }
3842
- if (name === "setDevice" && call.arguments.length > 0) {
3843
- const arg0 = call.arguments[0];
3844
- if (arg0 instanceof ValueExpression) {
3845
- const val = arg0.value.asString?.call(arg0.value);
3846
- if (typeof val === "string" && !this.deps.knownSlots.includes(val)) {
3847
- diags.push(
3848
- diag("error", "E3", `setDevice("${val}"): unknown slot. Known: ${this.deps.knownSlots.join(", ") || "(none)"}`, call)
3849
- );
3850
- }
3851
- }
3852
- }
3853
- if (name === "apiCall" && call.arguments.length > 0) {
3854
- const jsonArgIdx = 2;
3855
- const jsonArg = call.arguments[jsonArgIdx];
3856
- if (jsonArg instanceof ValueExpression) {
3857
- const val = jsonArg.value.asString?.call(jsonArg.value);
3858
- if (typeof val === "string") {
3859
- try {
3860
- JSON.parse(val);
3861
- } catch (e) {
3862
- diags.push(
3863
- diag("error", "E4", `${name}: arg ${jsonArgIdx} is not valid JSON \u2014 ${e.message}`, call)
3864
- );
3865
- }
3866
- }
3867
- }
3868
- }
3869
- if (name.startsWith(ASSERT_PREFIX) && !isFirstUiSeen()) {
3870
- diags.push(diag("warning", "W5", `${name}() called before any UI action \u2014 typo or wrong order?`, call));
3871
- }
3872
- recordCall(name);
3873
- for (const arg of call.arguments) {
3874
- this.checkExpression(arg, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3875
- }
3876
- }
3877
- checkArity(call, fn, diags) {
3878
- const max = fn.argTypes.length;
3879
- const min = fn.minArgs ?? max;
3880
- const got = call.arguments.length;
3881
- if (got < min) {
3882
- diags.push(
3883
- diag(
3884
- "error",
3885
- "E5",
3886
- `${fn.name}(): expected ${min === max ? `${min}` : `at least ${min}`} arg${min === 1 ? "" : "s"}, got ${got}`,
3887
- call
3888
- )
3889
- );
3890
- return;
3891
- }
3892
- if (!fn.variadic && got > max) {
3893
- diags.push(
3894
- diag(
3895
- "error",
3896
- "E5",
3897
- `${fn.name}(): expected at most ${max} arg${max === 1 ? "" : "s"}, got ${got}`,
3898
- call
3899
- )
3900
- );
3901
- }
3902
- }
3903
- /**
3904
- * For each provided arg up to argTypes.length, infer its static type and
3905
- * compare to the declared signature. Only emits E6 when the inferred type
3906
- * is UNAMBIGUOUS — we never flag "any" sources (variables, helper-call
3907
- * results, BinaryExpression, ArrayExpression).
3908
- */
3909
- checkArgTypes(call, fn, diags) {
3910
- const limit = Math.min(call.arguments.length, fn.argTypes.length);
3911
- for (let i = 0; i < limit; i++) {
3912
- const expected = fn.argTypes[i];
3913
- if (expected === "any") continue;
3914
- const inferred = this.inferExprType(call.arguments[i]);
3915
- if (inferred === "any") continue;
3916
- if (inferred === expected) continue;
3917
- if (expected === "boolean" && inferred === "number" || expected === "number" && inferred === "boolean") {
3918
- continue;
3919
- }
3920
- diags.push(
3921
- diag(
3922
- "error",
3923
- "E6",
3924
- `${fn.name}(): arg ${i} expected ${expected}, got ${describeInferred(call.arguments[i], inferred)}`,
3925
- call
3926
- )
3927
- );
3928
- }
3929
- }
3930
- /** Best-effort static type for an expression node. "any" = unknown. */
3931
- inferExprType(expr) {
3932
- if (expr instanceof ValueExpression) {
3933
- const v = expr.value;
3934
- if (v instanceof StringValue) return "string";
3935
- if (v instanceof NumberValue) return "number";
3936
- return "any";
3937
- }
3938
- if (expr instanceof VariableExpression) {
3939
- if (expr.name === "true" || expr.name === "false") return "boolean";
3940
- return "any";
3941
- }
3942
- if (expr instanceof FunctionalExpression) {
3943
- const callee = this.deps.registry.get(expr.name);
3944
- return callee ? callee.returnType : "any";
3945
- }
3946
- return "any";
3947
- }
3948
- isForbiddenStatement(stmt) {
3949
- return stmt instanceof ForStatement || stmt instanceof WhileStatement || stmt instanceof DoWhileStatement || stmt instanceof VarStatement || stmt instanceof PrintStatement || stmt instanceof BreakStatement || stmt instanceof ContinueStatement || stmt instanceof IncrementStatement || stmt instanceof ArrayAssignmentStatement;
3950
- }
3951
- isForbiddenExpression(expr) {
3952
- return expr instanceof IncrementExpression || expr instanceof UnaryExpression || expr instanceof ConditionalExpression || expr instanceof ObjectExpression || expr instanceof ArrayAccessExpression;
3953
- }
3954
- };
3955
- function isBlockMappable2(name) {
3956
- return name.startsWith("test_") || name.startsWith("flow_");
3957
- }
3958
- __name(isBlockMappable2, "isBlockMappable");
3959
- function describeInferred(expr, inferred) {
3960
- if (expr instanceof FunctionalExpression) {
3961
- return `${inferred} (${expr.name}() returns ${inferred})`;
3962
- }
3963
- if (expr instanceof ValueExpression) {
3964
- return `${inferred} literal`;
3965
- }
3966
- if (expr instanceof VariableExpression && (expr.name === "true" || expr.name === "false")) {
3967
- return `boolean literal \`${expr.name}\``;
3968
- }
3969
- return inferred;
3970
- }
3971
- __name(describeInferred, "describeInferred");
3972
- function diag(severity, code, message, node) {
3973
- const tok = node.token;
3974
- return {
3975
- severity,
3976
- code,
3977
- message,
3978
- line: tok?.getLine ? tok.getLine() : 0,
3979
- col: tok?.getColumn ? tok.getColumn() : 0
3980
- };
3981
- }
3982
- __name(diag, "diag");
3983
-
3984
- // src/dsl/function-registry.ts
3985
- var FunctionRegistry = class {
3986
- static {
3987
- __name(this, "FunctionRegistry");
3988
- }
3989
- map = /* @__PURE__ */ new Map();
3990
- register(fn) {
3991
- if (this.map.has(fn.name)) {
3992
- throw new Error(`Duplicate DSL function: ${fn.name}`);
3993
- }
3994
- this.map.set(fn.name, fn);
3995
- }
3996
- has(name) {
3997
- return this.map.has(name);
3998
- }
3999
- get(name) {
4000
- return this.map.get(name);
4001
- }
4002
- names() {
4003
- return [...this.map.keys()].sort();
4004
- }
4005
- };
4006
-
4007
- // src/dsl/functions/alerts.ts
4008
- function asString(x, fn, idx) {
4009
- if (typeof x !== "string") throw new Error(`${fn}(): arg ${idx} must be a string. Got ${typeof x}.`);
4010
- return x;
4011
- }
4012
- __name(asString, "asString");
4013
- var acceptAlert = {
4014
- name: "acceptAlert",
4015
- argTypes: ["string"],
4016
- returnType: "void",
4017
- minArgs: 0,
4018
- invoke: /* @__PURE__ */ __name(async (runtime, buttonArg) => {
4019
- const button = buttonArg !== void 0 ? asString(buttonArg, "acceptAlert", 0) : void 0;
4020
- await runtime.driver.acceptAlert(runtime.currentDeviceSlot, button);
4021
- }, "invoke")
4022
- };
4023
- var dismissAlert = {
4024
- name: "dismissAlert",
4025
- argTypes: [],
4026
- returnType: "void",
4027
- invoke: /* @__PURE__ */ __name(async (runtime) => {
4028
- await runtime.driver.dismissAlert(runtime.currentDeviceSlot);
4029
- }, "invoke")
4030
- };
4031
- var readAlert = {
4032
- name: "readAlert",
4033
- argTypes: [],
4034
- returnType: "string",
4035
- invoke: /* @__PURE__ */ __name(async (runtime) => {
4036
- const { text } = await runtime.driver.readAlert(runtime.currentDeviceSlot);
4037
- return text;
4038
- }, "invoke")
4039
- };
4040
- var ALERT_FUNCTIONS = [acceptAlert, dismissAlert, readAlert];
4041
-
4042
- // src/dsl/functions/asserts.ts
4043
- function asSelector(x, fn, idx) {
4044
- if (typeof x !== "object" || x === null) {
4045
- throw new Error(`${fn}(): arg ${idx} must be a Selector. Got ${typeof x}.`);
4046
- }
4047
- return x;
4048
- }
4049
- __name(asSelector, "asSelector");
4050
- function asNumber(x, fn, idx) {
4051
- if (typeof x !== "number") throw new Error(`${fn}(): arg ${idx} must be a number. Got ${typeof x}.`);
4052
- return x;
4053
- }
4054
- __name(asNumber, "asNumber");
4055
- var assertEqual = {
4056
- name: "assertEqual",
4057
- argTypes: ["any", "any"],
4058
- returnType: "void",
4059
- invoke: /* @__PURE__ */ __name((_runtime, actual, expected) => {
4060
- if (actual !== expected) {
4061
- throw new Error(
4062
- `assertEqual failed: actual ${JSON.stringify(actual)} !== expected ${JSON.stringify(expected)}`
4063
- );
4064
- }
4065
- }, "invoke")
4066
- };
4067
- var assertVisible = {
4068
- name: "assertVisible",
4069
- argTypes: ["selector"],
4070
- returnType: "void",
4071
- invoke: /* @__PURE__ */ __name(async (runtime, selectorArg) => {
4072
- const selector = asSelector(selectorArg, "assertVisible", 0);
4073
- const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
4074
- const r = runtime.selectorResolver.resolve(tree, selector);
4075
- if (!r.ok) {
4076
- throw new Error(
4077
- `assertVisible failed: ${r.reason}
4078
- selector: ${JSON.stringify(selector)}
4079
- candidates: ${JSON.stringify(r.candidates).slice(0, 600)}`
4080
- );
4081
- }
4082
- }, "invoke")
4083
- };
4084
- var assertCount = {
4085
- name: "assertCount",
4086
- argTypes: ["selector", "number"],
4087
- returnType: "void",
4088
- invoke: /* @__PURE__ */ __name(async (runtime, selectorArg, expectedArg) => {
4089
- const selector = asSelector(selectorArg, "assertCount", 0);
4090
- const expected = asNumber(expectedArg, "assertCount", 1);
4091
- const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
4092
- let count = 0;
4093
- while (true) {
4094
- const probe = { ...selector, ordinal: count };
4095
- const r = runtime.selectorResolver.resolve(tree, probe);
4096
- if (!r.ok) break;
4097
- count++;
4098
- if (count > 1e3) {
4099
- throw new Error(`assertCount: aborting after 1000 \u2014 selector too broad`);
4100
- }
4101
- }
4102
- if (count !== expected) {
4103
- throw new Error(
4104
- `assertCount failed: ${JSON.stringify(selector)} matched ${count} time(s), expected ${expected}`
4105
- );
4106
- }
4107
- }, "invoke")
4108
- };
4109
- async function assertEnabledState(runtime, selectorArg, expected, fnName) {
4110
- const selector = asSelector(selectorArg, fnName, 0);
4111
- const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
4112
- const r = runtime.selectorResolver.resolve(tree, selector);
4113
- if (!r.ok) {
4114
- throw new Error(
4115
- `${fnName} failed: selector did not match. ${r.reason}
4116
- selector: ${JSON.stringify(selector)}`
4117
- );
4118
- }
4119
- const node = r.node;
4120
- if (node.enabled === void 0) {
4121
- throw new Error(
4122
- `${fnName} failed: matched node does not report 'enabled' (likely a non-interactive widget or RN-side accessibility gap). Selector: ${JSON.stringify(selector)}`
4123
- );
2468
+ const node = r.node;
2469
+ if (node.enabled === void 0) {
2470
+ throw new Error(
2471
+ `${fnName} failed: matched node does not report 'enabled' (likely a non-interactive widget or RN-side accessibility gap). Selector: ${JSON.stringify(selector)}`
2472
+ );
4124
2473
  }
4125
2474
  if (node.enabled !== expected) {
4126
2475
  throw new Error(
@@ -4494,47 +2843,319 @@ var pressKey = {
4494
2843
  if (key !== "back" && key !== "home" && key !== "enter" && key !== "escape") {
4495
2844
  throw new Error(`pressKey(): key must be 'back'|'home'|'enter'|'escape'. Got "${key}".`);
4496
2845
  }
4497
- await runtime.driver.pressKey(runtime.currentDeviceSlot, key);
4498
- }, "invoke")
4499
- };
4500
- var waitFor = {
4501
- name: "waitFor",
4502
- argTypes: ["selector", "number"],
4503
- returnType: "void",
4504
- minArgs: 1,
4505
- invoke: /* @__PURE__ */ __name(async (runtime, selectorArg, timeoutMsArg) => {
4506
- const selector = asSelector2(selectorArg, "waitFor", 0);
4507
- const timeoutMs = timeoutMsArg === void 0 ? runtime.envConfig.defaultWaitForTimeoutMs : asNumber2(timeoutMsArg, "waitFor", 1);
4508
- await pollUntilFound(runtime, selector, timeoutMs);
4509
- }, "invoke")
4510
- };
4511
- var pause = {
4512
- name: "pause",
4513
- argTypes: ["number"],
4514
- returnType: "void",
4515
- invoke: /* @__PURE__ */ __name(async (_runtime, msArg) => {
4516
- const ms = asNumber2(msArg, "pause", 0);
4517
- await wait(ms);
4518
- }, "invoke")
2846
+ await runtime.driver.pressKey(runtime.currentDeviceSlot, key);
2847
+ }, "invoke")
2848
+ };
2849
+ var waitFor = {
2850
+ name: "waitFor",
2851
+ argTypes: ["selector", "number"],
2852
+ returnType: "void",
2853
+ minArgs: 1,
2854
+ invoke: /* @__PURE__ */ __name(async (runtime, selectorArg, timeoutMsArg) => {
2855
+ const selector = asSelector2(selectorArg, "waitFor", 0);
2856
+ const timeoutMs = timeoutMsArg === void 0 ? runtime.envConfig.defaultWaitForTimeoutMs : asNumber2(timeoutMsArg, "waitFor", 1);
2857
+ await pollUntilFound(runtime, selector, timeoutMs);
2858
+ }, "invoke")
2859
+ };
2860
+ var pause = {
2861
+ name: "pause",
2862
+ argTypes: ["number"],
2863
+ returnType: "void",
2864
+ invoke: /* @__PURE__ */ __name(async (_runtime, msArg) => {
2865
+ const ms = asNumber2(msArg, "pause", 0);
2866
+ await wait(ms);
2867
+ }, "invoke")
2868
+ };
2869
+ var UI_FUNCTIONS = [tap, type_, swipe, pressKey, waitFor, pause];
2870
+
2871
+ // src/dsl/functions/index.ts
2872
+ var ALL_DSL_FUNCTIONS = [
2873
+ ...DEVICE_FUNCTIONS,
2874
+ ...UI_FUNCTIONS,
2875
+ ...SELECTOR_FUNCTIONS,
2876
+ ...ALERT_FUNCTIONS,
2877
+ ...DATA_FUNCTIONS,
2878
+ ...ASSERT_FUNCTIONS,
2879
+ ...TIME_FUNCTIONS
2880
+ ];
2881
+ function buildDefaultRegistry() {
2882
+ const reg = new FunctionRegistry();
2883
+ for (const fn of ALL_DSL_FUNCTIONS) reg.register(fn);
2884
+ return reg;
2885
+ }
2886
+ __name(buildDefaultRegistry, "buildDefaultRegistry");
2887
+
2888
+ // src/dsl/linter.ts
2889
+ var ASSERT_PREFIX = "assert";
2890
+ var UI_CALL_NAMES = /* @__PURE__ */ new Set(["tap", "type", "swipe", "pressKey", "waitFor", "appLaunch", "openDeeplink"]);
2891
+ var DslLinter = class {
2892
+ constructor(deps) {
2893
+ this.deps = deps;
2894
+ }
2895
+ deps;
2896
+ static {
2897
+ __name(this, "DslLinter");
2898
+ }
2899
+ /**
2900
+ * Lint a parsed scenario AST: mobile-specific pass + the shared
2901
+ * validator engine over the same tree.
2902
+ *
2903
+ * @param ast entry-scenario AST
2904
+ * @param source raw source — the engine's validation session reads it
2905
+ * @param options cross-cutting context (user-function names, etc.)
2906
+ */
2907
+ lint(ast, source, options = {}) {
2908
+ const diags = [];
2909
+ const userFns = options.userFunctionNames ?? /* @__PURE__ */ new Set();
2910
+ for (const stmt of ast.statements) {
2911
+ if (stmt instanceof FunctionDefineStatement && isBlockMappable(stmt.name)) {
2912
+ this.lintFunctionBody(
2913
+ stmt,
2914
+ diags,
2915
+ userFns,
2916
+ /*allowReturn*/
2917
+ false
2918
+ );
2919
+ } else if (stmt instanceof FunctionDefineStatement) {
2920
+ this.lintFunctionBody(
2921
+ stmt,
2922
+ diags,
2923
+ userFns,
2924
+ /*allowReturn*/
2925
+ true
2926
+ );
2927
+ } else if (stmt instanceof MetaBlockStatement) {
2928
+ } else {
2929
+ diags.push(diag("error", "E2", `Only function definitions allowed at top level (got ${stmt.constructor.name})`, stmt));
2930
+ }
2931
+ }
2932
+ diags.push(...this.runEngine(ast, source ?? "", userFns));
2933
+ return diags;
2934
+ }
2935
+ /**
2936
+ * Cross-file E8 check — duplicate user-function names + shadowing
2937
+ * built-ins. Run ONCE per scenario load (not per file) using the
2938
+ * full origin list from ScenarioLoader.userFunctionOrigins.
2939
+ *
2940
+ * Each diagnostic is anchored to a specific origin (with path/line/col),
2941
+ * so production callers print "_helpers/seed.js:12:1 [E8] ...".
2942
+ */
2943
+ lintCrossFile(origins) {
2944
+ const diags = [];
2945
+ const byName = /* @__PURE__ */ new Map();
2946
+ for (const o of origins) {
2947
+ const list = byName.get(o.name) ?? [];
2948
+ list.push(o);
2949
+ byName.set(o.name, list);
2950
+ }
2951
+ for (const [name, list] of byName) {
2952
+ if (this.deps.registry.has(name)) {
2953
+ for (const o of list) {
2954
+ diags.push({
2955
+ severity: "error",
2956
+ code: "E8",
2957
+ message: `User function "${name}" shadows a built-in DSL function. Rename it.`,
2958
+ line: o.line,
2959
+ col: o.col,
2960
+ source: o.source
2961
+ });
2962
+ }
2963
+ continue;
2964
+ }
2965
+ if (list.length > 1) {
2966
+ const where = list.map((o) => `${o.source}:${o.line}`).join(", ");
2967
+ for (const o of list) {
2968
+ diags.push({
2969
+ severity: "error",
2970
+ code: "E8",
2971
+ message: `Duplicate user function "${name}". Defined at: ${where}`,
2972
+ line: o.line,
2973
+ col: o.col,
2974
+ source: o.source
2975
+ });
2976
+ }
2977
+ }
2978
+ }
2979
+ return diags;
2980
+ }
2981
+ /**
2982
+ * Lint a helper file. Same two layers as a scenario, but `return` is
2983
+ * allowed in any non-entry function body and all diagnostics are
2984
+ * tagged with helper.path.
2985
+ */
2986
+ lintHelper(helper, options = {}) {
2987
+ const diags = [];
2988
+ const userFns = options.userFunctionNames ?? /* @__PURE__ */ new Set();
2989
+ for (const stmt of helper.ast.statements) {
2990
+ if (stmt instanceof FunctionDefineStatement) {
2991
+ if (isBlockMappable(stmt.name)) {
2992
+ this.lintFunctionBody(
2993
+ stmt,
2994
+ diags,
2995
+ userFns,
2996
+ /*allowReturn*/
2997
+ false
2998
+ );
2999
+ } else {
3000
+ this.lintFunctionBody(
3001
+ stmt,
3002
+ diags,
3003
+ userFns,
3004
+ /*allowReturn*/
3005
+ true
3006
+ );
3007
+ }
3008
+ } else if (stmt instanceof MetaBlockStatement) {
3009
+ } else {
3010
+ diags.push(diag("error", "E2", `Only function definitions allowed at top level (got ${stmt.constructor.name})`, stmt));
3011
+ }
3012
+ }
3013
+ diags.push(...this.runEngine(helper.ast, helper.source ?? "", userFns));
3014
+ return diags.map((d) => ({ ...d, source: helper.path }));
3015
+ }
3016
+ // ---- shared engine ------------------------------------------------------
3017
+ /** Run the `@unotest/dsl/validator` engine with the mobile contracts.
3018
+ * Engine diagnostics map 1:1 into mobile `Diagnostic` rows with
3019
+ * `validator:<rule>` codes. */
3020
+ runEngine(ast, source, userFns) {
3021
+ const registry = new MobileDslRegistry(ALL_DSL_FUNCTIONS, userFns);
3022
+ return validateDsl(ast, source, false, registry, {
3023
+ disabledRules: MOBILE_DISABLED_ENGINE_RULES
3024
+ }).map((d) => ({
3025
+ severity: d.severity,
3026
+ code: `validator:${d.rule}`,
3027
+ message: d.message,
3028
+ line: d.line ?? 0,
3029
+ col: d.column ?? 0
3030
+ }));
3031
+ }
3032
+ // ---- mobile-specific pass ----------------------------------------------
3033
+ lintFunctionBody(fn, diags, userFns, allowReturn) {
3034
+ if (!(fn.body instanceof BlockStatement)) {
3035
+ diags.push(diag("error", "E2", `Function "${fn.name}" body must be a block`, fn));
3036
+ return;
3037
+ }
3038
+ let firstUiSeen = false;
3039
+ const recordCall = /* @__PURE__ */ __name((name) => {
3040
+ if (UI_CALL_NAMES.has(name)) firstUiSeen = true;
3041
+ }, "recordCall");
3042
+ for (const stmt of fn.body.statements) {
3043
+ this.checkStatement(stmt, diags, recordCall, () => firstUiSeen, userFns, allowReturn);
3044
+ }
3045
+ }
3046
+ checkStatement(stmt, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
3047
+ if (stmt instanceof ReturnStatement) {
3048
+ if (!allowReturn) {
3049
+ diags.push(diag("error", "E2", `'return' is not allowed inside test_*/flow_* function bodies (only in helpers)`, stmt));
3050
+ return;
3051
+ }
3052
+ const expr = stmt.expression;
3053
+ if (expr) this.checkExpression(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3054
+ return;
3055
+ }
3056
+ if (this.isForbiddenStatement(stmt)) {
3057
+ diags.push(diag("error", "E2", `Forbidden statement type "${stmt.constructor.name}" (D-4 subset)`, stmt));
3058
+ return;
3059
+ }
3060
+ if (stmt instanceof AssignmentStatement) {
3061
+ this.checkExpression(stmt.expression, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3062
+ return;
3063
+ }
3064
+ if (stmt instanceof FunctionStatement) {
3065
+ const call = stmt.functionalExpression;
3066
+ if (!(call instanceof FunctionalExpression)) {
3067
+ diags.push(diag("error", "E2", `Method chains are not allowed (D-4 subset)`, stmt));
3068
+ return;
3069
+ }
3070
+ this.checkCall(call, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3071
+ return;
3072
+ }
3073
+ if (stmt instanceof StepStatement) {
3074
+ this.checkStatement(stmt.body, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3075
+ return;
3076
+ }
3077
+ if (stmt instanceof IfStatement) {
3078
+ this.checkExpression(stmt.expression, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3079
+ this.checkStatement(stmt.ifStatement, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3080
+ if (stmt.elseStatement) this.checkStatement(stmt.elseStatement, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3081
+ return;
3082
+ }
3083
+ if (stmt instanceof BlockStatement) {
3084
+ for (const s of stmt.statements) this.checkStatement(s, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3085
+ return;
3086
+ }
3087
+ if (stmt instanceof FunctionDefineStatement) {
3088
+ diags.push(diag("error", "E2", "Nested function definitions are not allowed", stmt));
3089
+ return;
3090
+ }
3091
+ if (stmt instanceof MetaBlockStatement) return;
3092
+ diags.push(diag("error", "E2", `Unsupported statement "${stmt.constructor.name}"`, stmt));
3093
+ }
3094
+ checkExpression(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
3095
+ if (expr instanceof FunctionalExpression) {
3096
+ this.checkCall(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3097
+ return;
3098
+ }
3099
+ if (expr instanceof BinaryExpression) {
3100
+ this.checkExpression(expr.expr1, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3101
+ this.checkExpression(expr.expr2, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3102
+ return;
3103
+ }
3104
+ if (expr instanceof ArrayExpression) {
3105
+ for (const e of expr.elements) this.checkExpression(e, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3106
+ return;
3107
+ }
3108
+ if (expr instanceof ValueExpression || expr instanceof VariableExpression) return;
3109
+ if (this.isForbiddenExpression(expr)) {
3110
+ diags.push(diag("error", "E2", `Forbidden expression "${expr.constructor.name}" (D-4 subset)`, expr));
3111
+ return;
3112
+ }
3113
+ diags.push(diag("error", "E2", `Unsupported expression "${expr.constructor.name}"`, expr));
3114
+ }
3115
+ checkCall(call, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
3116
+ const name = call.name;
3117
+ if (name === "setDevice" && call.arguments.length > 0) {
3118
+ const arg0 = call.arguments[0];
3119
+ if (arg0 instanceof ValueExpression) {
3120
+ const val = arg0.value.asString?.call(arg0.value);
3121
+ if (typeof val === "string" && !this.deps.knownSlots.includes(val)) {
3122
+ diags.push(
3123
+ diag("error", "E3", `setDevice("${val}"): unknown slot. Known: ${this.deps.knownSlots.join(", ") || "(none)"}`, call)
3124
+ );
3125
+ }
3126
+ }
3127
+ }
3128
+ if (name.startsWith(ASSERT_PREFIX) && !isFirstUiSeen()) {
3129
+ diags.push(diag("warning", "W5", `${name}() called before any UI action \u2014 typo or wrong order?`, call));
3130
+ }
3131
+ recordCall(name);
3132
+ for (const arg of call.arguments) {
3133
+ this.checkExpression(arg, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
3134
+ }
3135
+ }
3136
+ isForbiddenStatement(stmt) {
3137
+ return stmt instanceof ForStatement || stmt instanceof WhileStatement || stmt instanceof DoWhileStatement || stmt instanceof VarStatement || stmt instanceof PrintStatement || stmt instanceof BreakStatement || stmt instanceof ContinueStatement || stmt instanceof IncrementStatement || stmt instanceof ArrayAssignmentStatement;
3138
+ }
3139
+ isForbiddenExpression(expr) {
3140
+ return expr instanceof IncrementExpression || expr instanceof UnaryExpression || expr instanceof ConditionalExpression || expr instanceof ObjectExpression || expr instanceof ArrayAccessExpression || // Post-vendor @unotest/dsl extensions the mobile frozen subset rejects.
3141
+ expr instanceof MemberCallExpression || expr instanceof PropertyAccessExpression;
3142
+ }
4519
3143
  };
4520
- var UI_FUNCTIONS = [tap, type_, swipe, pressKey, waitFor, pause];
4521
-
4522
- // src/dsl/functions/index.ts
4523
- var ALL_DSL_FUNCTIONS = [
4524
- ...DEVICE_FUNCTIONS,
4525
- ...UI_FUNCTIONS,
4526
- ...SELECTOR_FUNCTIONS,
4527
- ...ALERT_FUNCTIONS,
4528
- ...DATA_FUNCTIONS,
4529
- ...ASSERT_FUNCTIONS,
4530
- ...TIME_FUNCTIONS
4531
- ];
4532
- function buildDefaultRegistry() {
4533
- const reg = new FunctionRegistry();
4534
- for (const fn of ALL_DSL_FUNCTIONS) reg.register(fn);
4535
- return reg;
3144
+ function isBlockMappable(name) {
3145
+ return name.startsWith("test_") || name.startsWith("flow_");
4536
3146
  }
4537
- __name(buildDefaultRegistry, "buildDefaultRegistry");
3147
+ __name(isBlockMappable, "isBlockMappable");
3148
+ function diag(severity, code, message, node) {
3149
+ const tok = node.token;
3150
+ return {
3151
+ severity,
3152
+ code,
3153
+ message,
3154
+ line: tok?.getLine ? tok.getLine() : 0,
3155
+ col: tok?.getColumn ? tok.getColumn() : 0
3156
+ };
3157
+ }
3158
+ __name(diag, "diag");
4538
3159
 
4539
3160
  // src/dsl/test-runtime-manager.ts
4540
3161
  var TestRuntimeManager = class {
@@ -4695,849 +3316,77 @@ var TestRuntimeManager = class {
4695
3316
  if (!active.ttlTimer) return;
4696
3317
  const clear = this.deps.clearTimeoutFn ?? clearTimeout;
4697
3318
  clear(active.ttlTimer);
4698
- active.ttlTimer = null;
4699
- }
4700
- requireActive(id) {
4701
- const a = this.runtimes.get(id);
4702
- if (!a) throw new Error(`Runtime "${id}" not found (already aborted or never started)`);
4703
- return a;
4704
- }
4705
- };
4706
-
4707
- // src/runner/fs-scenario-repository.ts
4708
- import { readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync } from "fs";
4709
- import { readdir } from "fs/promises";
4710
- import { dirname as dirname2, resolve, join as join2, sep } from "path";
4711
- var FileSystemScenarioRepository = class {
4712
- static {
4713
- __name(this, "FileSystemScenarioRepository");
4714
- }
4715
- scenariosDir;
4716
- constructor(deps) {
4717
- this.scenariosDir = resolve(deps.scenariosDir);
4718
- }
4719
- async list() {
4720
- const out = [];
4721
- await this.walk(this.scenariosDir, "", out);
4722
- return out.sort();
4723
- }
4724
- async walk(dir, relPrefix, out) {
4725
- const entries = await readdir(dir, { withFileTypes: true });
4726
- for (const entry of entries) {
4727
- if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
4728
- const abs = join2(dir, entry.name);
4729
- const rel = relPrefix === "" ? entry.name : `${relPrefix}/${entry.name}`;
4730
- if (entry.isDirectory()) {
4731
- await this.walk(abs, rel, out);
4732
- } else if (entry.isFile() && entry.name.endsWith(".js")) {
4733
- out.push(rel.slice(0, -".js".length));
4734
- }
4735
- }
4736
- }
4737
- async load(name) {
4738
- const path = this.pathFor(name);
4739
- const source = readFileSync2(path, "utf8");
4740
- return { name, source };
4741
- }
4742
- async has(name) {
4743
- return existsSync2(this.pathFor(name));
4744
- }
4745
- async save(name, source, opts) {
4746
- const path = this.pathFor(name);
4747
- if (existsSync2(path) && !opts?.overwrite) {
4748
- throw new Error(`scenario "${name}" already exists at ${path}`);
4749
- }
4750
- mkdirSync2(dirname2(path), { recursive: true });
4751
- writeFileSync(path, source);
4752
- return path;
4753
- }
4754
- pathFor(name) {
4755
- if (name.includes("..") || name.startsWith("/") || name.includes("\\")) {
4756
- throw new Error(`Invalid scenario name "${name}"`);
4757
- }
4758
- const relPath = name.split("/").join(sep) + ".js";
4759
- return join2(this.scenariosDir, relPath);
4760
- }
4761
- };
4762
-
4763
- // vendor/dsl/parser/token.ts
4764
- var Token = class {
4765
- static {
4766
- __name(this, "Token");
4767
- }
4768
- text;
4769
- type;
4770
- column;
4771
- line;
4772
- constructor(tokenType, text, column, line) {
4773
- this.type = tokenType;
4774
- this.text = text;
4775
- this.column = column;
4776
- this.line = line;
4777
- }
4778
- getText() {
4779
- return this.text;
4780
- }
4781
- getType() {
4782
- return this.type;
4783
- }
4784
- getLine() {
4785
- return this.line;
4786
- }
4787
- getColumn() {
4788
- return this.column;
4789
- }
4790
- // Переопределение метода toString
4791
- toString() {
4792
- return `Token(type: ${TokenType[this.type]}, text: "${this.text}", line: ${this.line}, column: ${this.column})`;
4793
- }
4794
- };
4795
-
4796
- // vendor/dsl/parser/tools.ts
4797
- function isNumber(char) {
4798
- const num = Number(char);
4799
- return !isNaN(num) && char.trim() === num.toString();
4800
- }
4801
- __name(isNumber, "isNumber");
4802
- function isLetter(char) {
4803
- return /^[a-zA-Zа-яА-ЯёЁ]$/.test(char);
4804
- }
4805
- __name(isLetter, "isLetter");
4806
- function isLetterInWord(char) {
4807
- return /^[a-zA-Zа-яА-ЯёЁ0-9_$]$/.test(char);
4808
- }
4809
- __name(isLetterInWord, "isLetterInWord");
4810
-
4811
- // vendor/dsl/parser/lexer.ts
4812
- var Lexer = class {
4813
- static {
4814
- __name(this, "Lexer");
4815
- }
4816
- OPERATOR_CHARS = "+-*/(){}[]=<>!&|,:;";
4817
- OPERATORS = /* @__PURE__ */ new Map([
4818
- ["+", 14 /* PLUS */],
4819
- ["++", 15 /* PLUSPLUS */],
4820
- ["-", 16 /* MINUS */],
4821
- ["--", 17 /* MINUSMINUS */],
4822
- ["*", 18 /* STAR */],
4823
- ["/", 19 /* SLASH */],
4824
- ["(", 32 /* LPAREN */],
4825
- [")", 33 /* RPAREN */],
4826
- ["{", 34 /* LBRACE */],
4827
- ["}", 35 /* RBRACE */],
4828
- ["[", 36 /* LBRACKET */],
4829
- ["]", 37 /* RBRACKET */],
4830
- ["=", 20 /* EQ */],
4831
- ["<", 24 /* LT */],
4832
- [">", 26 /* GT */],
4833
- [",", 38 /* COMMA */],
4834
- [":", 39 /* COLON */],
4835
- [";", 40 /* SEMICOLON */],
4836
- ["!", 22 /* EXCL */],
4837
- ["&", 30 /* AMP */],
4838
- ["|", 28 /* BAR */],
4839
- ["==", 21 /* EQEQ */],
4840
- ["!=", 23 /* EXCLEQ */],
4841
- ["<=", 25 /* LTEQ */],
4842
- [">=", 27 /* GTEQ */],
4843
- ["&&", 31 /* AMPAMP */],
4844
- ["||", 29 /* BARBAR */]
4845
- ]);
4846
- input;
4847
- length;
4848
- tokens = [];
4849
- pos = 0;
4850
- column = 1;
4851
- line = 1;
4852
- constructor(input) {
4853
- this.input = input;
4854
- this.length = input.length;
4855
- }
4856
- tokenize() {
4857
- while (this.pos < this.length) {
4858
- const current = this.peek(0);
4859
- if (isNumber(current)) {
4860
- this.tokenizeNumber();
4861
- } else if (isLetter(current)) {
4862
- this.tokenizeWord();
4863
- } else if (current === '"' || current === "'") {
4864
- this.tokenizeText(current);
4865
- } else if (this.peekGroup(3) === "//@") {
4866
- this.tokenizeMetaWord();
4867
- } else if (this.OPERATOR_CHARS.indexOf(current) !== -1) {
4868
- this.tokenizeOperator();
4869
- } else {
4870
- this.next();
4871
- }
4872
- }
4873
- return this.tokens;
4874
- }
4875
- next() {
4876
- const currentChar = this.input.charAt(this.pos);
4877
- this.pos++;
4878
- if (currentChar == "\n") {
4879
- this.line++;
4880
- this.column = 1;
4881
- } else {
4882
- this.column++;
4883
- }
4884
- return this.peek(0);
4885
- }
4886
- peek(relativePosition) {
4887
- const position = this.pos + relativePosition;
4888
- if (position >= this.length) {
4889
- return "\0";
4890
- }
4891
- return this.input.charAt(position);
4892
- }
4893
- peekGroup(count) {
4894
- const buffer = [];
4895
- for (let i = 0; i < count; i++) {
4896
- buffer.push(this.peek(i));
4897
- }
4898
- return buffer.join("");
4899
- }
4900
- addToken(type, text, column, line) {
4901
- this.tokens.push(new Token(type, text, column - 1, line));
4902
- }
4903
- tokenizeNumber() {
4904
- const column = this.column;
4905
- const line = this.line;
4906
- const buffer = [];
4907
- let current = this.peek(0);
4908
- while (current !== "\0") {
4909
- if (current == ".") {
4910
- if (buffer.indexOf(".") !== -1) {
4911
- throw new Error("Invalid float number");
4912
- }
4913
- } else if (!isNumber(current)) {
4914
- break;
4915
- }
4916
- buffer.push(current);
4917
- current = this.next();
4918
- }
4919
- this.addToken(0 /* NUMBER */, buffer.join(""), column, line);
4920
- }
4921
- consumeWord() {
4922
- const buffer = [];
4923
- let current = this.peek(0);
4924
- while (current !== "\0") {
4925
- if (!isLetterInWord(current)) {
4926
- break;
4927
- }
4928
- buffer.push(current);
4929
- current = this.next();
4930
- }
4931
- return buffer.join("");
4932
- }
4933
- tokenizeMetaWord() {
4934
- const column = this.column;
4935
- const line = this.line;
4936
- this.next();
4937
- this.next();
4938
- this.next();
4939
- const word = this.consumeWord();
4940
- if (word.startsWith("end")) {
4941
- this.addToken(42 /* CLOSE_META_BLOCK */, word, column, line);
4942
- } else {
4943
- this.addToken(41 /* META_BLOCK */, word, column, line);
4944
- }
4945
- }
4946
- tokenizeWord() {
4947
- const column = this.column;
4948
- const line = this.line;
4949
- const word = this.consumeWord();
4950
- switch (word) {
4951
- case "var":
4952
- this.addToken(3 /* VAR */, "", column, line);
4953
- break;
4954
- case "print":
4955
- this.addToken(4 /* PRINT */, "", column, line);
4956
- break;
4957
- case "if":
4958
- this.addToken(5 /* IF */, "", column, line);
4959
- break;
4960
- case "else":
4961
- this.addToken(6 /* ELSE */, "", column, line);
4962
- break;
4963
- case "for":
4964
- this.addToken(8 /* FOR */, "", column, line);
4965
- break;
4966
- case "while":
4967
- this.addToken(7 /* WHILE */, "", column, line);
4968
- break;
4969
- case "do":
4970
- this.addToken(9 /* DO */, "", column, line);
4971
- break;
4972
- case "break":
4973
- this.addToken(10 /* BREAK */, "", column, line);
4974
- break;
4975
- case "continue":
4976
- this.addToken(11 /* CONTINUE */, "", column, line);
4977
- break;
4978
- case "function":
4979
- this.addToken(12 /* FUNCTION */, "", column, line);
4980
- break;
4981
- case "return":
4982
- this.addToken(13 /* RETURN */, "", column, line);
4983
- break;
4984
- default:
4985
- this.addToken(1 /* WORD */, word, column, line);
4986
- }
4987
- }
4988
- tokenizeText(char) {
4989
- this.next();
4990
- const column = this.column;
4991
- const line = this.line;
4992
- const buffer = [];
4993
- let current = this.peek(0);
4994
- while (current !== "\0") {
4995
- if (current == "\\") {
4996
- current = this.next();
4997
- switch (current) {
4998
- case char:
4999
- current = this.next();
5000
- buffer.push(char);
5001
- continue;
5002
- case "n":
5003
- current = this.next();
5004
- buffer.push("\n");
5005
- continue;
5006
- case "t":
5007
- current = this.next();
5008
- buffer.push(" ");
5009
- continue;
5010
- }
5011
- buffer.push("\\");
5012
- continue;
5013
- }
5014
- if (current == char) {
5015
- break;
5016
- }
5017
- buffer.push(current);
5018
- current = this.next();
5019
- }
5020
- if (current === "\0") {
5021
- throw new Error("Missing close quote in string literal");
5022
- }
5023
- this.next();
5024
- const text = buffer.join("");
5025
- this.addToken(2 /* TEXT */, text, column, line);
5026
- }
5027
- tokenizeComment() {
5028
- let current = this.peek(0);
5029
- while ("\r\n\0".indexOf(current) == -1) {
5030
- current = this.next();
5031
- }
5032
- }
5033
- tokenizeMultilineComment() {
5034
- let current = this.peek(0);
5035
- while (current != "\0") {
5036
- if (current == "*" && this.peek(1) == "/") break;
5037
- current = this.next();
5038
- }
5039
- if (current == "\0") throw new Error("Missing close tag in comment");
5040
- this.next();
5041
- this.next();
5042
- }
5043
- tokenizeOperator() {
5044
- let current = this.peek(0);
5045
- if (current == "/") {
5046
- if (this.peek(1) == "/") {
5047
- this.next();
5048
- this.next();
5049
- this.tokenizeComment();
5050
- return;
5051
- } else if (this.peek(1) == "*") {
5052
- this.next();
5053
- this.next();
5054
- this.tokenizeMultilineComment();
5055
- return;
5056
- }
5057
- }
5058
- const buffer = [];
5059
- while (current !== "\0") {
5060
- const text2 = buffer.join("");
5061
- const column = this.column;
5062
- const line = this.line;
5063
- if (!this.OPERATORS.has(text2 + current) && text2 != "") {
5064
- const type = this.OPERATORS.get(text2);
5065
- if (!type) {
5066
- throw new Error(`Invalid tokenizeOperator ${text2}`);
5067
- }
5068
- this.addToken(type, "", column, line);
5069
- return;
5070
- }
5071
- buffer.push(current);
5072
- current = this.next();
5073
- }
5074
- const text = buffer.join("");
5075
- if (text !== "") {
5076
- const type = this.OPERATORS.get(text);
5077
- if (type) {
5078
- this.addToken(type, "", this.column, this.line);
5079
- return;
5080
- }
5081
- }
5082
- throw new Error("Invalid tokenizeOperator: unexpected end of input");
5083
- }
5084
- };
5085
-
5086
- // vendor/dsl/parser/parser.ts
5087
- var Parser = class _Parser {
5088
- static {
5089
- __name(this, "Parser");
5090
- }
5091
- EOF = new Token(43 /* EOF */, "", -1, -1);
5092
- tokens = [];
5093
- pos = 0;
5094
- size;
5095
- constructor(tokens) {
5096
- this.tokens = tokens;
5097
- this.size = tokens.length;
5098
- }
5099
- parse() {
5100
- const current = this.get(0);
5101
- const result = new BlockStatement(current);
5102
- while (!this.match(43 /* EOF */)) {
5103
- result.add(this.statement());
5104
- this.match(40 /* SEMICOLON */);
5105
- }
5106
- return result;
5107
- }
5108
- statementOrBlock() {
5109
- if (this.get(0).getType() === 34 /* LBRACE */) {
5110
- return this.block();
5111
- }
5112
- return this.statement();
5113
- }
5114
- blockMetaBlock(name) {
5115
- const current = this.get(0);
5116
- const block = new BlockStatement(current);
5117
- while (!this.match(42 /* CLOSE_META_BLOCK */)) {
5118
- block.add(this.statement());
5119
- this.match(40 /* SEMICOLON */);
5120
- }
5121
- return block;
5122
- }
5123
- metaBlock() {
5124
- const current = this.get(0);
5125
- const name = this.consume(41 /* META_BLOCK */).getText();
5126
- this.consume(32 /* LPAREN */);
5127
- const argNames = [];
5128
- while (!this.match(33 /* RPAREN */)) {
5129
- argNames.push(this.consume(2 /* TEXT */).getText());
5130
- this.match(38 /* COMMA */);
5131
- }
5132
- const body = this.blockMetaBlock(name);
5133
- return new MetaBlockStatement(name, argNames, body, current);
5134
- }
5135
- block() {
5136
- const current = this.get(0);
5137
- const block = new BlockStatement(current);
5138
- this.consume(34 /* LBRACE */);
5139
- while (!this.match(35 /* RBRACE */)) {
5140
- block.add(this.statement());
5141
- this.match(40 /* SEMICOLON */);
5142
- }
5143
- return block;
5144
- }
5145
- statement() {
5146
- const current = this.get(0);
5147
- if (this.match(3 /* VAR */)) {
5148
- return this.varStatement(current);
5149
- }
5150
- if (this.match(4 /* PRINT */)) {
5151
- return new PrintStatement(this.expression(), current);
5152
- }
5153
- if (this.match(5 /* IF */)) {
5154
- return this.ifElse(current);
5155
- }
5156
- if (this.match(7 /* WHILE */)) {
5157
- return this.whileStatement(current);
5158
- }
5159
- if (this.match(9 /* DO */)) {
5160
- return this.doWhileStatement(current);
5161
- }
5162
- if (this.match(10 /* BREAK */)) {
5163
- return new BreakStatement(current);
5164
- }
5165
- if (this.match(11 /* CONTINUE */)) {
5166
- return new ContinueStatement(current);
5167
- }
5168
- if (this.match(13 /* RETURN */)) {
5169
- return new ReturnStatement(this.expression(), current);
5170
- }
5171
- if (this.match(8 /* FOR */)) {
5172
- return this.forStatement(current);
5173
- }
5174
- if (this.match(12 /* FUNCTION */)) {
5175
- return this.functionDefine(current);
5176
- }
5177
- if (this.lookMatch(0, 41 /* META_BLOCK */)) {
5178
- return this.metaBlock();
5179
- }
5180
- if (this.lookMatch(0, 1 /* WORD */) && this.lookMatch(1, 32 /* LPAREN */)) {
5181
- return new FunctionStatement(this.functionExpression(current), current);
5182
- }
5183
- if (this.match(15 /* PLUSPLUS */)) {
5184
- const variable = new VariableExpression(this.consume(1 /* WORD */).getText(), current);
5185
- return new IncrementStatement(
5186
- new IncrementExpression(variable, 0 /* PREFIX_INCREMENT */, current),
5187
- current
5188
- );
5189
- }
5190
- if (this.match(17 /* MINUSMINUS */)) {
5191
- const variable = new VariableExpression(this.consume(1 /* WORD */).getText(), current);
5192
- return new IncrementStatement(
5193
- new IncrementExpression(variable, 1 /* PREFIX_DECREMENT */, current),
5194
- current
5195
- );
5196
- }
5197
- if (this.lookMatch(0, 1 /* WORD */) && (this.lookMatch(1, 15 /* PLUSPLUS */) || this.lookMatch(1, 17 /* MINUSMINUS */))) {
5198
- const varToken = this.consume(1 /* WORD */);
5199
- const varName = varToken.getText();
5200
- if (this.match(15 /* PLUSPLUS */)) {
5201
- throw new Error(
5202
- `Postfix increment '${varName}++' is not supported. Use '${varName} = ${varName} + 1' instead. ${varToken.toString()}`
5203
- );
5204
- } else if (this.match(17 /* MINUSMINUS */)) {
5205
- throw new Error(
5206
- `Postfix decrement '${varName}--' is not supported. Use '${varName} = ${varName} - 1' instead. ${varToken.toString()}`
5207
- );
5208
- }
5209
- }
5210
- return this.assignmentStatement();
5211
- }
5212
- static RESERVED_WORDS = /* @__PURE__ */ new Set([
5213
- "true",
5214
- "false",
5215
- "null",
5216
- "undefined",
5217
- "let",
5218
- "const",
5219
- "class",
5220
- "new",
5221
- "delete",
5222
- "typeof",
5223
- "instanceof",
5224
- "in",
5225
- "of",
5226
- "import",
5227
- "export",
5228
- "default",
5229
- "extends",
5230
- "super",
5231
- "this",
5232
- "void",
5233
- "throw",
5234
- "try",
5235
- "catch",
5236
- "finally",
5237
- "switch",
5238
- "case",
5239
- "with",
5240
- "debugger",
5241
- "yield",
5242
- "await"
5243
- ]);
5244
- varStatement(token) {
5245
- const declarators = [];
5246
- do {
5247
- const nameToken = this.consume(1 /* WORD */);
5248
- const name = nameToken.getText();
5249
- if (_Parser.RESERVED_WORDS.has(name)) {
5250
- throw new Error(
5251
- `Variable name '${name}' is a reserved JavaScript keyword. Use a different name. ${nameToken.toString()}`
5252
- );
5253
- }
5254
- let init = null;
5255
- if (this.match(20 /* EQ */)) {
5256
- init = this.expression();
5257
- }
5258
- declarators.push({ name, init });
5259
- } while (this.match(38 /* COMMA */));
5260
- return new VarStatement(declarators, token);
5261
- }
5262
- assignmentStatement() {
5263
- const current = this.get(0);
5264
- if (this.lookMatch(0, 1 /* WORD */) && this.lookMatch(1, 20 /* EQ */)) {
5265
- const variable = this.consume(1 /* WORD */).getText();
5266
- this.consume(20 /* EQ */);
5267
- return new AssignmentStatement(variable, this.expression(), current);
5268
- }
5269
- if (this.lookMatch(0, 1 /* WORD */) && this.lookMatch(1, 36 /* LBRACKET */)) {
5270
- const array = this.elementExpression(current);
5271
- this.consume(20 /* EQ */);
5272
- return new ArrayAssignmentStatement(array, this.expression(), current);
5273
- }
5274
- throw new Error(`Unknown statement ${this.get(0)}`);
5275
- }
5276
- ifElse(token) {
5277
- const condition = this.expression();
5278
- const ifStatement = this.statementOrBlock();
5279
- let elseStatement;
5280
- if (this.match(6 /* ELSE */)) {
5281
- elseStatement = this.statementOrBlock();
5282
- } else {
5283
- elseStatement = null;
5284
- }
5285
- return new IfStatement(condition, ifStatement, elseStatement, token);
5286
- }
5287
- doWhileStatement(token) {
5288
- const statement = this.statementOrBlock();
5289
- this.consume(7 /* WHILE */);
5290
- const condition = this.expression();
5291
- return new DoWhileStatement(condition, statement, token);
5292
- }
5293
- whileStatement(token) {
5294
- const condition = this.expression();
5295
- const statement = this.statementOrBlock();
5296
- return new WhileStatement(condition, statement, token);
5297
- }
5298
- forStatement(token) {
5299
- this.consume(32 /* LPAREN */);
5300
- let initialization;
5301
- if (this.match(3 /* VAR */)) {
5302
- throw new Error(
5303
- `'var' in for-loop initialization is not supported by the Blockly converter. Use plain assignment: 'for (k = 1; k <= N; k = k + 1)'. ${this.get(-1).toString()}`
5304
- );
5305
- } else {
5306
- initialization = this.assignmentStatement();
5307
- }
5308
- this.consume(40 /* SEMICOLON */);
5309
- const termination = this.expression();
5310
- this.consume(40 /* SEMICOLON */);
5311
- const increment = this.statement();
5312
- this.consume(33 /* RPAREN */);
5313
- const block = this.statementOrBlock();
5314
- return new ForStatement(initialization, termination, increment, block, token);
5315
- }
5316
- functionDefine(token) {
5317
- const name = this.consume(1 /* WORD */).getText();
5318
- this.consume(32 /* LPAREN */);
5319
- const argNames = [];
5320
- while (!this.match(33 /* RPAREN */)) {
5321
- argNames.push(this.consume(1 /* WORD */).getText());
5322
- this.match(38 /* COMMA */);
5323
- }
5324
- const body = this.statementOrBlock();
5325
- return new FunctionDefineStatement(name, argNames, body, token);
5326
- }
5327
- functionExpression(token) {
5328
- const name = this.consume(1 /* WORD */).getText();
5329
- this.consume(32 /* LPAREN */);
5330
- const func = new FunctionalExpression(name, [], token);
5331
- while (!this.match(33 /* RPAREN */)) {
5332
- func.addArgument(this.expression());
5333
- this.match(38 /* COMMA */);
5334
- }
5335
- return func;
5336
- }
5337
- elementExpression(token) {
5338
- const variable = this.consume(1 /* WORD */).getText();
5339
- const indexes = [];
5340
- do {
5341
- this.consume(36 /* LBRACKET */);
5342
- indexes.push(this.expression());
5343
- this.consume(37 /* RBRACKET */);
5344
- } while (this.lookMatch(0, 36 /* LBRACKET */));
5345
- return new ArrayAccessExpression(variable, indexes, token);
5346
- }
5347
- arrayExpression(token) {
5348
- this.consume(36 /* LBRACKET */);
5349
- const elements = [];
5350
- while (!this.match(37 /* RBRACKET */)) {
5351
- elements.push(this.expression());
5352
- this.match(38 /* COMMA */);
5353
- }
5354
- return new ArrayExpression(elements, token);
5355
- }
5356
- objectExpression(token) {
5357
- this.consume(34 /* LBRACE */);
5358
- const properties = /* @__PURE__ */ new Map();
5359
- while (!this.match(35 /* RBRACE */)) {
5360
- let key;
5361
- if (this.lookMatch(0, 1 /* WORD */)) {
5362
- key = this.consume(1 /* WORD */).getText();
5363
- } else if (this.lookMatch(0, 2 /* TEXT */)) {
5364
- key = this.consume(2 /* TEXT */).getText();
5365
- } else {
5366
- throw new Error("Expected property key (word or string)");
5367
- }
5368
- this.consume(39 /* COLON */);
5369
- const value = this.expression();
5370
- properties.set(key, value);
5371
- this.match(38 /* COMMA */);
5372
- }
5373
- return new ObjectExpression(properties, token);
5374
- }
5375
- expression() {
5376
- return this.logicalOr();
3319
+ active.ttlTimer = null;
5377
3320
  }
5378
- logicalOr() {
5379
- let result = this.logicalAnd();
5380
- while (this.match(29 /* BARBAR */)) {
5381
- const current = this.get(0);
5382
- result = new ConditionalExpression("||" /* OR */, result, this.logicalAnd(), current);
5383
- }
5384
- return result;
3321
+ requireActive(id) {
3322
+ const a = this.runtimes.get(id);
3323
+ if (!a) throw new Error(`Runtime "${id}" not found (already aborted or never started)`);
3324
+ return a;
5385
3325
  }
5386
- logicalAnd() {
5387
- let result = this.equality();
5388
- while (this.match(31 /* AMPAMP */)) {
5389
- const current = this.get(0);
5390
- result = new ConditionalExpression("&&" /* AND */, result, this.equality(), current);
5391
- }
5392
- return result;
3326
+ };
3327
+
3328
+ // src/runner/fs-scenario-repository.ts
3329
+ import { readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync } from "fs";
3330
+ import { readdir } from "fs/promises";
3331
+ import { dirname as dirname2, resolve, join as join2, sep } from "path";
3332
+ var FileSystemScenarioRepository = class {
3333
+ static {
3334
+ __name(this, "FileSystemScenarioRepository");
5393
3335
  }
5394
- equality() {
5395
- const current = this.get(0);
5396
- const result = this.conditional();
5397
- if (this.match(21 /* EQEQ */)) {
5398
- return new ConditionalExpression("==" /* EQ */, result, this.conditional(), current);
5399
- }
5400
- if (this.match(23 /* EXCLEQ */)) {
5401
- return new ConditionalExpression("!=" /* NEQ */, result, this.conditional(), current);
5402
- }
5403
- return result;
3336
+ scenariosDir;
3337
+ constructor(deps) {
3338
+ this.scenariosDir = resolve(deps.scenariosDir);
5404
3339
  }
5405
- conditional() {
5406
- let result = this.additive();
5407
- while (this.lookMatch(0, 24 /* LT */) || this.lookMatch(0, 25 /* LTEQ */) || this.lookMatch(0, 26 /* GT */) || this.lookMatch(0, 27 /* GTEQ */)) {
5408
- const current = this.get(0);
5409
- if (this.match(24 /* LT */)) {
5410
- result = new ConditionalExpression("<" /* LT */, result, this.additive(), current);
5411
- continue;
5412
- }
5413
- if (this.match(25 /* LTEQ */)) {
5414
- result = new ConditionalExpression("<=" /* LTE */, result, this.additive(), current);
5415
- continue;
5416
- }
5417
- if (this.match(26 /* GT */)) {
5418
- result = new ConditionalExpression(">" /* GT */, result, this.additive(), current);
5419
- continue;
5420
- }
5421
- if (this.match(27 /* GTEQ */)) {
5422
- result = new ConditionalExpression(">=" /* GTE */, result, this.additive(), current);
5423
- continue;
5424
- }
5425
- }
5426
- return result;
3340
+ async list() {
3341
+ const out = [];
3342
+ await this.walk(this.scenariosDir, "", out);
3343
+ return out.sort();
5427
3344
  }
5428
- additive() {
5429
- let result = this.multiplicate();
5430
- while (this.lookMatch(0, 14 /* PLUS */) || this.lookMatch(0, 16 /* MINUS */)) {
5431
- if (this.match(14 /* PLUS */)) {
5432
- const opToken2 = this.get(-1);
5433
- result = new BinaryExpression("+", result, this.multiplicate(), opToken2);
5434
- continue;
5435
- }
5436
- this.consume(16 /* MINUS */);
5437
- const opToken = this.get(-1);
5438
- result = new BinaryExpression("-", result, this.multiplicate(), opToken);
5439
- }
5440
- return result;
5441
- }
5442
- multiplicate() {
5443
- let result = this.unary();
5444
- while (this.lookMatch(0, 18 /* STAR */) || this.lookMatch(0, 19 /* SLASH */)) {
5445
- if (this.match(18 /* STAR */)) {
5446
- const opToken2 = this.get(-1);
5447
- result = new BinaryExpression("*", result, this.unary(), opToken2);
5448
- continue;
3345
+ async walk(dir, relPrefix, out) {
3346
+ const entries = await readdir(dir, { withFileTypes: true });
3347
+ for (const entry of entries) {
3348
+ if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
3349
+ const abs = join2(dir, entry.name);
3350
+ const rel = relPrefix === "" ? entry.name : `${relPrefix}/${entry.name}`;
3351
+ if (entry.isDirectory()) {
3352
+ await this.walk(abs, rel, out);
3353
+ } else if (entry.isFile() && entry.name.endsWith(".js")) {
3354
+ out.push(rel.slice(0, -".js".length));
5449
3355
  }
5450
- this.consume(19 /* SLASH */);
5451
- const opToken = this.get(-1);
5452
- result = new BinaryExpression("/", result, this.unary(), opToken);
5453
- }
5454
- return result;
5455
- }
5456
- unary() {
5457
- const current = this.get(0);
5458
- if (this.match(16 /* MINUS */)) {
5459
- return new UnaryExpression("-", this.primary(), current);
5460
- }
5461
- if (this.match(15 /* PLUSPLUS */)) {
5462
- const variable = new VariableExpression(this.consume(1 /* WORD */).getText(), current);
5463
- return new IncrementExpression(variable, 0 /* PREFIX_INCREMENT */, current);
5464
3356
  }
5465
- if (this.match(17 /* MINUSMINUS */)) {
5466
- const variable = new VariableExpression(this.consume(1 /* WORD */).getText(), current);
5467
- return new IncrementExpression(variable, 1 /* PREFIX_DECREMENT */, current);
5468
- }
5469
- return this.primary();
5470
3357
  }
5471
- primary() {
5472
- const current = this.get(0);
5473
- if (this.match(0 /* NUMBER */)) {
5474
- return new ValueExpression(current.getText(), current);
5475
- }
5476
- if (this.lookMatch(0, 1 /* WORD */) && this.lookMatch(1, 32 /* LPAREN */)) {
5477
- return this.functionExpression(current);
5478
- }
5479
- if (this.lookMatch(0, 1 /* WORD */) && this.lookMatch(1, 36 /* LBRACKET */)) {
5480
- return this.elementExpression(current);
5481
- }
5482
- if (this.lookMatch(0, 36 /* LBRACKET */)) {
5483
- return this.arrayExpression(current);
5484
- }
5485
- if (this.lookMatch(0, 34 /* LBRACE */)) {
5486
- return this.objectExpression(current);
5487
- }
5488
- if (this.match(1 /* WORD */)) {
5489
- const variable = new VariableExpression(current.getText(), current);
5490
- if (this.match(15 /* PLUSPLUS */)) {
5491
- throw new Error(
5492
- `Postfix increment '${current.getText()}++' is not supported. Use '${current.getText()} = ${current.getText()} + 1' instead. ${current.toString()}`
5493
- );
5494
- }
5495
- if (this.match(17 /* MINUSMINUS */)) {
5496
- throw new Error(
5497
- `Postfix decrement '${current.getText()}--' is not supported. Use '${current.getText()} = ${current.getText()} - 1' instead. ${current.toString()}`
5498
- );
5499
- }
5500
- return variable;
5501
- }
5502
- if (this.match(2 /* TEXT */)) {
5503
- return new ValueExpression(current.getText(), current);
5504
- }
5505
- if (this.match(32 /* LPAREN */)) {
5506
- const result = this.expression();
5507
- this.match(33 /* RPAREN */);
5508
- return result;
5509
- }
5510
- throw new Error(`Invalid token ${current}`);
3358
+ async load(name) {
3359
+ const path = this.pathFor(name);
3360
+ const source = readFileSync2(path, "utf8");
3361
+ return { name, source };
5511
3362
  }
5512
- consume(type) {
5513
- const current = this.get(0);
5514
- if (type != current.getType()) {
5515
- throw new Error(`Token ${current} doesn't match token type ${TokenType[type]}`);
5516
- }
5517
- this.pos++;
5518
- return current;
3363
+ async has(name) {
3364
+ return existsSync2(this.pathFor(name));
5519
3365
  }
5520
- match(type) {
5521
- const current = this.get(0);
5522
- if (type != current.getType()) {
5523
- return false;
3366
+ async save(name, source, opts) {
3367
+ const path = this.pathFor(name);
3368
+ if (existsSync2(path) && !opts?.overwrite) {
3369
+ throw new Error(`scenario "${name}" already exists at ${path}`);
5524
3370
  }
5525
- this.pos++;
5526
- return true;
5527
- }
5528
- lookMatch(pos, type) {
5529
- return this.get(pos).getType() == type;
3371
+ mkdirSync2(dirname2(path), { recursive: true });
3372
+ writeFileSync(path, source);
3373
+ return path;
5530
3374
  }
5531
- get(relativePosition) {
5532
- const position = this.pos + relativePosition;
5533
- if (position >= this.size) {
5534
- return this.EOF;
3375
+ pathFor(name) {
3376
+ if (name.includes("..") || name.startsWith("/") || name.includes("\\")) {
3377
+ throw new Error(`Invalid scenario name "${name}"`);
5535
3378
  }
5536
- return this.tokens[position];
3379
+ const relPath = name.split("/").join(sep) + ".js";
3380
+ return join2(this.scenariosDir, relPath);
5537
3381
  }
5538
3382
  };
5539
3383
 
5540
3384
  // src/runner/scenario-loader.ts
3385
+ import { Lexer } from "@unotest/dsl";
3386
+ import { Parser } from "@unotest/dsl";
3387
+ import {
3388
+ FunctionDefineStatement as FunctionDefineStatement2
3389
+ } from "@unotest/dsl";
5541
3390
  var ScenarioLoader = class {
5542
3391
  constructor(repo, helpers) {
5543
3392
  this.repo = repo;
@@ -5570,7 +3419,7 @@ function resolveEntryFunctionName(scenarioName, prefix = "test") {
5570
3419
  __name(resolveEntryFunctionName, "resolveEntryFunctionName");
5571
3420
  function collectOrigins(program, sourcePath, out) {
5572
3421
  for (const stmt of program.statements) {
5573
- if (stmt instanceof FunctionDefineStatement) {
3422
+ if (stmt instanceof FunctionDefineStatement2) {
5574
3423
  const tok = stmt.token;
5575
3424
  out.push({
5576
3425
  name: stmt.name,
@@ -5587,6 +3436,8 @@ __name(collectOrigins, "collectOrigins");
5587
3436
  import { readdir as readdir2, readFile } from "fs/promises";
5588
3437
  import { existsSync as existsSync3 } from "fs";
5589
3438
  import { join as join3, relative } from "path";
3439
+ import { Lexer as Lexer2 } from "@unotest/dsl";
3440
+ import { Parser as Parser2 } from "@unotest/dsl";
5590
3441
  var FileSystemHelperRepository = class {
5591
3442
  static {
5592
3443
  __name(this, "FileSystemHelperRepository");
@@ -5603,8 +3454,8 @@ var FileSystemHelperRepository = class {
5603
3454
  const out = [];
5604
3455
  for (const absolutePath of files) {
5605
3456
  const source = await readFile(absolutePath, "utf8");
5606
- const tokens = new Lexer(source).tokenize();
5607
- const ast = new Parser(tokens).parse();
3457
+ const tokens = new Lexer2(source).tokenize();
3458
+ const ast = new Parser2(tokens).parse();
5608
3459
  out.push({
5609
3460
  path: relative(this.scenariosDir, absolutePath),
5610
3461
  absolutePath,
@@ -5893,14 +3744,15 @@ var DslViewService = class {
5893
3744
  generate(session, log) {
5894
3745
  const warnings = [];
5895
3746
  const lines = [];
5896
- lines.push(renderHeader(session));
3747
+ lines.push(`// ${session.title ?? session.scenarioName}`);
5897
3748
  lines.push(`function test_${session.scenarioName.replace(/[^a-zA-Z0-9_]/g, "_")}() {`);
5898
- lines.push(` setDevice(${JSON.stringify(session.device)});`);
5899
- let openSection = null;
3749
+ lines.push(` step("setup", () => {`);
3750
+ lines.push(` setDevice(${JSON.stringify(session.device)});`);
3751
+ let openSection = "setup";
5900
3752
  for (const entry of log.entries) {
5901
3753
  if (entry.section !== openSection) {
5902
- if (openSection !== null) lines.push(" //@endcollapse");
5903
- lines.push(` //@collapse(${JSON.stringify(entry.section)})`);
3754
+ lines.push(" });");
3755
+ lines.push(` step(${JSON.stringify(entry.section)}, () => {`);
5904
3756
  openSection = entry.section;
5905
3757
  }
5906
3758
  const result = renderEntry(entry);
@@ -5910,7 +3762,7 @@ var DslViewService = class {
5910
3762
  type: "NO_DSL_PRIMITIVE",
5911
3763
  message: result.reason
5912
3764
  });
5913
- lines.push(` // SKIPPED (${result.reason}) \u2014 replace with getByTestId/getByText/getByLabel`);
3765
+ lines.push(` // SKIPPED (${result.reason}) \u2014 replace with getByTestId/getByText/getByLabel`);
5914
3766
  continue;
5915
3767
  }
5916
3768
  if (entry.stability === "fragile") {
@@ -5923,22 +3775,13 @@ var DslViewService = class {
5923
3775
  for (const w of result.warnings ?? []) {
5924
3776
  warnings.push({ ...w, entryId: entry.entryId });
5925
3777
  }
5926
- lines.push(" " + result.stmt);
3778
+ lines.push(" " + result.stmt);
5927
3779
  }
5928
- if (openSection !== null) lines.push(" //@endcollapse");
3780
+ lines.push(" });");
5929
3781
  lines.push("}");
5930
3782
  return { draftDsl: lines.join("\n") + "\n", warnings };
5931
3783
  }
5932
3784
  };
5933
- function renderHeader(session) {
5934
- const title = session.title ?? session.scenarioName;
5935
- return [
5936
- `// id-${session.scenarioName}`,
5937
- `// ${title}`,
5938
- `// #4287f5`
5939
- ].join("\n");
5940
- }
5941
- __name(renderHeader, "renderHeader");
5942
3785
  function renderEntry(entry) {
5943
3786
  const sel = /* @__PURE__ */ __name((s) => {
5944
3787
  const out = formatSelector(s);
@@ -6199,7 +4042,7 @@ async function buildApp(opts = {}) {
6199
4042
  await Promise.allSettled(slots.map((slot) => active.testRuntime.driver.shutdown(slot)));
6200
4043
  }, "onAbort")
6201
4044
  });
6202
- const scenariosDirAbs = resolve3(opts.scenariosDir ?? "unotest/e2e");
4045
+ const scenariosDirAbs = resolve3(opts.scenariosDir ?? MOBILE_SCENARIOS_DIR);
6203
4046
  const scenarioRepository = opts.scenarioRepository ?? new FileSystemScenarioRepository({ scenariosDir: scenariosDirAbs });
6204
4047
  const helperRepository = opts.helperRepository ?? new FileSystemHelperRepository({ scenariosDir: scenariosDirAbs });
6205
4048
  const scenarioLoader = new ScenarioLoader(scenarioRepository, helperRepository);
@@ -7030,37 +4873,7 @@ var AppInstallTool = class extends BaseTool {
7030
4873
  import { z as z5 } from "zod";
7031
4874
 
7032
4875
  // src/dsl/variable-scope.ts
7033
- var VariableScope = class {
7034
- static {
7035
- __name(this, "VariableScope");
7036
- }
7037
- stack = [];
7038
- current = /* @__PURE__ */ new Map();
7039
- push() {
7040
- this.stack.push(new Map(this.current));
7041
- }
7042
- pop() {
7043
- const popped = this.stack.pop();
7044
- if (popped === void 0) throw new Error("VariableScope.pop(): underflow (more pops than pushes)");
7045
- this.current = popped;
7046
- }
7047
- get(name) {
7048
- return this.current.get(name);
7049
- }
7050
- has(name) {
7051
- return this.current.has(name);
7052
- }
7053
- set(name, value) {
7054
- this.current.set(name, value);
7055
- }
7056
- toObject() {
7057
- return Object.fromEntries(this.current.entries());
7058
- }
7059
- /** Depth of the push-stack. 0 at construction; +1 per push, -1 per pop. */
7060
- depth() {
7061
- return this.stack.length;
7062
- }
7063
- };
4876
+ import { VariableScope } from "@unotest/dsl/executor";
7064
4877
 
7065
4878
  // src/dsl/runtime.ts
7066
4879
  var TestRuntime = class {
@@ -7180,7 +4993,7 @@ var RunTestTool = class extends BaseTool {
7180
4993
  "run_test",
7181
4994
  {
7182
4995
  title: "Run a DSL scenario (with optional step/pause modes)",
7183
- description: "Loads unotest/e2e/<name>.js, parses + lints, then runs the function 'test_<name>' through AstExecutor. mode='auto' runs to completion or failure; mode='step' pauses after every statement. pauseOnFailure (default true) keeps simulators and WDA sessions alive on error so they can be inspected.",
4996
+ description: "Loads unotest/e2e-mobile/<name>.js, parses + lints, then runs the function 'test_<name>' through AstExecutor. mode='auto' runs to completion or failure; mode='step' pauses after every statement. pauseOnFailure (default true) keeps simulators and WDA sessions alive on error so they can be inspected.",
7184
4997
  inputSchema: {
7185
4998
  name: z5.string(),
7186
4999
  mode: z5.enum(["auto", "step"]).optional(),
@@ -7208,7 +5021,7 @@ var RunTestTool = class extends BaseTool {
7208
5021
  if (!loaded2.userFunctionNames.has(testFn)) {
7209
5022
  return this.failJson({
7210
5023
  status: "entry_not_found",
7211
- message: `Entry function "${testFn}" not found in ${name}.js. Convention: unotest/e2e/${name}.js \u2192 function ${testFn}().`
5024
+ message: `Entry function "${testFn}" not found in ${name}.js. Convention: unotest/e2e-mobile/${name}.js \u2192 function ${testFn}().`
7212
5025
  });
7213
5026
  }
7214
5027
  const runId = `e2e-${randomUUID2().slice(0, 8)}`;
@@ -7347,19 +5160,404 @@ var ListRuntimesTool = class extends BaseTool {
7347
5160
  }
7348
5161
  };
7349
5162
 
7350
- // src/mcp/tools/explore.tool.ts
5163
+ // src/mcp/tools/attach.tool.ts
5164
+ import { z as z6 } from "zod";
5165
+
5166
+ // src/mcp/attach-runtime.ts
5167
+ import {
5168
+ COMMANDS_FILE,
5169
+ HEARTBEAT_FILE,
5170
+ RUNTIME_FILE
5171
+ } from "@unotest/protocol";
5172
+ import { appendFile, mkdir, readFile as readFile2 } from "fs/promises";
5173
+ import { existsSync as existsSync6, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
5174
+ import { dirname as dirname4, join as join6 } from "path";
5175
+ var HEARTBEAT_STALE_MS = 3e4;
5176
+ function runDir(projectRoot, runId) {
5177
+ return join6(projectRoot, MOBILE_RUNS_DIR, runId);
5178
+ }
5179
+ __name(runDir, "runDir");
5180
+ async function readMobileRuntimeFile(projectRoot, runId) {
5181
+ try {
5182
+ return JSON.parse(
5183
+ await readFile2(join6(runDir(projectRoot, runId), RUNTIME_FILE), "utf8")
5184
+ );
5185
+ } catch {
5186
+ return null;
5187
+ }
5188
+ }
5189
+ __name(readMobileRuntimeFile, "readMobileRuntimeFile");
5190
+ async function findShareableMobileRun(projectRoot, nowFn = Date.now) {
5191
+ const root = join6(projectRoot, MOBILE_RUNS_DIR);
5192
+ if (!existsSync6(root)) return null;
5193
+ const now = nowFn();
5194
+ let best = null;
5195
+ for (const name of readdirSync3(root)) {
5196
+ const hb = join6(root, name, HEARTBEAT_FILE);
5197
+ if (!existsSync6(hb)) continue;
5198
+ let mtimeMs = 0;
5199
+ try {
5200
+ mtimeMs = statSync3(hb).mtimeMs;
5201
+ } catch {
5202
+ continue;
5203
+ }
5204
+ if (now - mtimeMs > HEARTBEAT_STALE_MS) continue;
5205
+ const snap = await readMobileRuntimeFile(projectRoot, name);
5206
+ if (!snap?.deviceSlot) continue;
5207
+ if (snap.state === "completed" || snap.state === "failed" || snap.state === "aborted") {
5208
+ continue;
5209
+ }
5210
+ if (!best || mtimeMs > best.mtimeMs) best = { runtimeId: name, snap, mtimeMs };
5211
+ }
5212
+ return best ? { runtimeId: best.runtimeId, snap: best.snap } : null;
5213
+ }
5214
+ __name(findShareableMobileRun, "findShareableMobileRun");
5215
+ async function appendDbgCommand(projectRoot, runId, cmd) {
5216
+ const path = join6(runDir(projectRoot, runId), COMMANDS_FILE);
5217
+ await mkdir(dirname4(path), { recursive: true });
5218
+ await appendFile(path, JSON.stringify(cmd) + "\n", "utf8");
5219
+ }
5220
+ __name(appendDbgCommand, "appendDbgCommand");
5221
+ function nextCommandId() {
5222
+ return Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8);
5223
+ }
5224
+ __name(nextCommandId, "nextCommandId");
5225
+ var AttachState = class {
5226
+ static {
5227
+ __name(this, "AttachState");
5228
+ }
5229
+ runId = null;
5230
+ slot = null;
5231
+ attach(runId, slot) {
5232
+ this.runId = runId;
5233
+ this.slot = slot;
5234
+ }
5235
+ detach() {
5236
+ this.runId = null;
5237
+ this.slot = null;
5238
+ }
5239
+ attachedRunId() {
5240
+ return this.runId;
5241
+ }
5242
+ attachedSlot() {
5243
+ return this.slot;
5244
+ }
5245
+ };
5246
+
5247
+ // src/mcp/tools/attach.tool.ts
5248
+ var AttachBaseTool = class extends BaseTool {
5249
+ constructor(services, attach, projectRoot) {
5250
+ super(services);
5251
+ this.attach = attach;
5252
+ this.projectRoot = projectRoot;
5253
+ }
5254
+ attach;
5255
+ projectRoot;
5256
+ static {
5257
+ __name(this, "AttachBaseTool");
5258
+ }
5259
+ };
5260
+ var GetActiveContextTool = class extends AttachBaseTool {
5261
+ static {
5262
+ __name(this, "GetActiveContextTool");
5263
+ }
5264
+ register(server) {
5265
+ server.registerTool(
5266
+ "get_active_context",
5267
+ {
5268
+ title: "Active mobile context",
5269
+ description: "Reports whether you are attached to a live mobile debug session and, if not, whether one is JOINABLE. When a human is debugging a scenario in the viewer (Debug mode), `attachable` names its runtimeId + device slot + state \u2014 call attach_debug_session to co-drive the SAME device, then a11y_tree / screenshot that slot. Returns attachable=null when no live debug session exists (start your own with run_test).",
5270
+ inputSchema: {}
5271
+ },
5272
+ async () => this.tracked("get_active_context", {}, async () => {
5273
+ const attachedRunId = this.attach.attachedRunId();
5274
+ const found = await findShareableMobileRun(this.projectRoot);
5275
+ return this.okJson({
5276
+ attached: attachedRunId ? { runtimeId: attachedRunId, slot: this.attach.attachedSlot() } : null,
5277
+ attachable: found ? {
5278
+ runtimeId: found.runtimeId,
5279
+ slot: found.snap.deviceSlot ?? null,
5280
+ state: found.snap.state,
5281
+ wheel: found.snap.wheel ?? "human"
5282
+ } : null
5283
+ });
5284
+ })
5285
+ );
5286
+ }
5287
+ };
5288
+ var AttachDebugSessionTool = class extends AttachBaseTool {
5289
+ static {
5290
+ __name(this, "AttachDebugSessionTool");
5291
+ }
5292
+ register(server) {
5293
+ server.registerTool(
5294
+ "attach_debug_session",
5295
+ {
5296
+ title: "Attach to a mobile debug session",
5297
+ description: "Join a human's live mobile debug run so you BOTH drive the SAME device (collaborative debugging). Omit runtimeId to auto-pick the single live debug session. Afterwards a11y_tree / screenshot / find_element on the run's device slot show what the human is debugging; take_wheel before you act. The run must be started in Debug mode (the viewer's Debug button) \u2014 a normal run advertises no device slot. Call detach_debug_session to disengage.",
5298
+ inputSchema: { runtimeId: z6.string().optional() }
5299
+ },
5300
+ async ({ runtimeId }) => this.tracked("attach_debug_session", { runtimeId }, async () => {
5301
+ let id = runtimeId;
5302
+ let snap = id ? await readMobileRuntimeFile(this.projectRoot, id) : null;
5303
+ if (!id) {
5304
+ const found = await findShareableMobileRun(this.projectRoot);
5305
+ if (!found) {
5306
+ return this.okJson({
5307
+ status: "no_session",
5308
+ reason: "no live mobile debug session to attach to \u2014 start one from the viewer's Debug button, or run_test here."
5309
+ });
5310
+ }
5311
+ id = found.runtimeId;
5312
+ snap = found.snap;
5313
+ }
5314
+ if (!snap) {
5315
+ return this.okJson({
5316
+ status: "no_session",
5317
+ reason: `no runtime.json for "${id}" \u2014 the run may have ended.`
5318
+ });
5319
+ }
5320
+ if (!snap.deviceSlot) {
5321
+ return this.okJson({
5322
+ status: "not_attachable",
5323
+ runtimeId: id,
5324
+ reason: "this run advertises no device slot \u2014 only Debug-mode runs are attachable."
5325
+ });
5326
+ }
5327
+ this.attach.attach(id, snap.deviceSlot);
5328
+ return this.okJson({
5329
+ status: "attached",
5330
+ runtimeId: id,
5331
+ slot: snap.deviceSlot,
5332
+ state: snap.state,
5333
+ wheel: snap.wheel ?? "human"
5334
+ });
5335
+ })
5336
+ );
5337
+ }
5338
+ };
5339
+ var DetachDebugSessionTool = class extends AttachBaseTool {
5340
+ static {
5341
+ __name(this, "DetachDebugSessionTool");
5342
+ }
5343
+ register(server) {
5344
+ server.registerTool(
5345
+ "detach_debug_session",
5346
+ {
5347
+ title: "Detach from the mobile debug session",
5348
+ description: "Disengage from the live debug session you joined with attach_debug_session. Does not stop the human's run \u2014 only clears your attachment so the device tools no longer default to its slot.",
5349
+ inputSchema: {}
5350
+ },
5351
+ async () => this.tracked("detach_debug_session", {}, async () => {
5352
+ const was = this.attach.attachedRunId();
5353
+ this.attach.detach();
5354
+ return this.okJson({ status: "detached", wasAttachedTo: was });
5355
+ })
5356
+ );
5357
+ }
5358
+ };
5359
+ var WheelTool = class extends AttachBaseTool {
5360
+ constructor(services, attach, projectRoot, owner) {
5361
+ super(services, attach, projectRoot);
5362
+ this.owner = owner;
5363
+ }
5364
+ owner;
5365
+ static {
5366
+ __name(this, "WheelTool");
5367
+ }
5368
+ register(server) {
5369
+ const name = this.owner === "agent" ? "take_wheel" : "release_wheel";
5370
+ server.registerTool(
5371
+ name,
5372
+ {
5373
+ title: name === "take_wheel" ? "Take the wheel" : "Release the wheel",
5374
+ description: this.owner === "agent" ? "Take the collaborative control token so you may act on the human's debug device. The viewer shows an 'Agent driving' badge and the human can take it back any time. Omit runtimeId to use the session you attached to." : "Release the control token back to the human (the host). Omit runtimeId to use the session you attached to.",
5375
+ inputSchema: { runtimeId: z6.string().optional() }
5376
+ },
5377
+ async ({ runtimeId }) => this.tracked(name, { runtimeId }, async () => {
5378
+ const id = runtimeId ?? this.attach.attachedRunId();
5379
+ if (!id) {
5380
+ return this.okJson({
5381
+ status: "no_target",
5382
+ reason: "not attached \u2014 call attach_debug_session first, or pass runtimeId."
5383
+ });
5384
+ }
5385
+ await appendDbgCommand(this.projectRoot, id, {
5386
+ id: nextCommandId(),
5387
+ cmd: "wheel",
5388
+ owner: this.owner
5389
+ });
5390
+ return this.okJson({ status: "ok", runtimeId: id, owner: this.owner });
5391
+ })
5392
+ );
5393
+ }
5394
+ };
5395
+
5396
+ // src/mcp/tools/failure.tool.ts
7351
5397
  import { z as z7 } from "zod";
7352
5398
 
5399
+ // src/mcp/failure-bundle-reader.ts
5400
+ import {
5401
+ PICKER_SNAPSHOT_FILE,
5402
+ RUN_MANIFEST_FILE
5403
+ } from "@unotest/protocol";
5404
+ import { existsSync as existsSync7, readdirSync as readdirSync4 } from "fs";
5405
+ import { readFile as readFile3 } from "fs/promises";
5406
+ import { join as join7 } from "path";
5407
+ var FAILURE_SCREENSHOT_FILE = "screenshot.png";
5408
+ function runDir2(projectRoot, runId) {
5409
+ return join7(projectRoot, MOBILE_RUNS_DIR, runId);
5410
+ }
5411
+ __name(runDir2, "runDir");
5412
+ async function readManifest(projectRoot, runId) {
5413
+ try {
5414
+ return JSON.parse(
5415
+ await readFile3(join7(runDir2(projectRoot, runId), RUN_MANIFEST_FILE), "utf8")
5416
+ );
5417
+ } catch {
5418
+ return null;
5419
+ }
5420
+ }
5421
+ __name(readManifest, "readManifest");
5422
+ async function listMobileFailures(projectRoot) {
5423
+ const root = join7(projectRoot, MOBILE_RUNS_DIR);
5424
+ if (!existsSync7(root)) return [];
5425
+ const out = [];
5426
+ for (const runId of readdirSync4(root)) {
5427
+ const snap = await readMobileRuntimeFile(projectRoot, runId);
5428
+ if (!snap) continue;
5429
+ const failed = snap.state === "failed" || snap.state === "paused-failure" || snap.lastFailure != null;
5430
+ if (!failed) continue;
5431
+ const manifest = await readManifest(projectRoot, runId);
5432
+ out.push({
5433
+ runId,
5434
+ scenario: manifest?.ref ?? snap.scenario ?? runId,
5435
+ state: snap.state,
5436
+ error: snap.lastFailure?.error ?? null,
5437
+ location: snap.lastFailure ? { line: snap.lastFailure.line, col: snap.lastFailure.col } : null,
5438
+ hasScreenshot: existsSync7(join7(runDir2(projectRoot, runId), FAILURE_SCREENSHOT_FILE)),
5439
+ hasOutline: existsSync7(join7(runDir2(projectRoot, runId), PICKER_SNAPSHOT_FILE))
5440
+ });
5441
+ }
5442
+ return out;
5443
+ }
5444
+ __name(listMobileFailures, "listMobileFailures");
5445
+ async function readFailureScreenshot(projectRoot, runId) {
5446
+ const path = join7(runDir2(projectRoot, runId), FAILURE_SCREENSHOT_FILE);
5447
+ if (!existsSync7(path)) return null;
5448
+ try {
5449
+ return await readFile3(path);
5450
+ } catch {
5451
+ return null;
5452
+ }
5453
+ }
5454
+ __name(readFailureScreenshot, "readFailureScreenshot");
5455
+ async function readFailureOutline(projectRoot, runId) {
5456
+ const path = join7(runDir2(projectRoot, runId), PICKER_SNAPSHOT_FILE);
5457
+ if (!existsSync7(path)) return null;
5458
+ try {
5459
+ return await readFile3(path, "utf8");
5460
+ } catch {
5461
+ return null;
5462
+ }
5463
+ }
5464
+ __name(readFailureOutline, "readFailureOutline");
5465
+
5466
+ // src/mcp/tools/failure.tool.ts
5467
+ var FailureBaseTool = class extends BaseTool {
5468
+ constructor(services, projectRoot) {
5469
+ super(services);
5470
+ this.projectRoot = projectRoot;
5471
+ }
5472
+ projectRoot;
5473
+ static {
5474
+ __name(this, "FailureBaseTool");
5475
+ }
5476
+ };
5477
+ var ListFailuresTool = class extends FailureBaseTool {
5478
+ static {
5479
+ __name(this, "ListFailuresTool");
5480
+ }
5481
+ register(server) {
5482
+ server.registerTool(
5483
+ "list_failures",
5484
+ {
5485
+ title: "List failed runs",
5486
+ description: "Lists recent runs that ended in (or are paused at) a failure, with the scenario name, error message, and failing line:col. Each entry says whether a failure screenshot and an a11y outline are available \u2014 fetch them with get_failure_screenshot / get_failure_a11y by runtimeId.",
5487
+ inputSchema: {}
5488
+ },
5489
+ async () => this.tracked("list_failures", {}, async () => {
5490
+ return this.okJson({ failures: await listMobileFailures(this.projectRoot) });
5491
+ })
5492
+ );
5493
+ }
5494
+ };
5495
+ var GetFailureScreenshotTool = class extends FailureBaseTool {
5496
+ static {
5497
+ __name(this, "GetFailureScreenshotTool");
5498
+ }
5499
+ register(server) {
5500
+ server.registerTool(
5501
+ "get_failure_screenshot",
5502
+ {
5503
+ title: "Failure screenshot",
5504
+ description: "Returns the device screenshot captured at the failure point of a run (as inline MCP image content). Pass the runtimeId from list_failures. Returns a not_found status when the run has no failure screenshot.",
5505
+ inputSchema: { runtimeId: z7.string() }
5506
+ },
5507
+ async ({ runtimeId }) => this.tracked("get_failure_screenshot", { runtimeId }, async () => {
5508
+ const png = await readFailureScreenshot(this.projectRoot, runtimeId);
5509
+ if (!png) {
5510
+ return this.okJson({ status: "not_found", runtimeId });
5511
+ }
5512
+ return {
5513
+ content: [
5514
+ {
5515
+ type: "image",
5516
+ data: png.toString("base64"),
5517
+ mimeType: "image/png"
5518
+ }
5519
+ ]
5520
+ };
5521
+ })
5522
+ );
5523
+ }
5524
+ };
5525
+ var GetFailureA11yTool = class extends FailureBaseTool {
5526
+ static {
5527
+ __name(this, "GetFailureA11yTool");
5528
+ }
5529
+ register(server) {
5530
+ server.registerTool(
5531
+ "get_failure_a11y",
5532
+ {
5533
+ title: "Failure a11y outline",
5534
+ description: "Returns the accessibility outline of the device at the failure/pause point of a run (the same compact outline as a11y_tree). Pass the runtimeId from list_failures. Returns a not_found status when no outline was captured.",
5535
+ inputSchema: { runtimeId: z7.string() }
5536
+ },
5537
+ async ({ runtimeId }) => this.tracked("get_failure_a11y", { runtimeId }, async () => {
5538
+ const outline = await readFailureOutline(this.projectRoot, runtimeId);
5539
+ if (outline === null) {
5540
+ return this.okJson({ status: "not_found", runtimeId });
5541
+ }
5542
+ return this.ok(outline);
5543
+ })
5544
+ );
5545
+ }
5546
+ };
5547
+
5548
+ // src/mcp/tools/explore.tool.ts
5549
+ import { z as z9 } from "zod";
5550
+
7353
5551
  // src/mcp/tools/selector-param.ts
7354
- import { z as z6 } from "zod";
5552
+ import { z as z8 } from "zod";
7355
5553
  var SelectorShape = {
7356
- testId: z6.string().optional(),
7357
- text: z6.string().optional(),
7358
- label: z6.string().optional(),
7359
- ordinal: z6.number().int().nonnegative().optional(),
7360
- pointPercent: z6.object({
7361
- x: z6.number().min(0).max(1),
7362
- y: z6.number().min(0).max(1)
5554
+ testId: z8.string().optional(),
5555
+ text: z8.string().optional(),
5556
+ label: z8.string().optional(),
5557
+ ordinal: z8.number().int().nonnegative().optional(),
5558
+ pointPercent: z8.object({
5559
+ x: z8.number().min(0).max(1),
5560
+ y: z8.number().min(0).max(1)
7363
5561
  }).optional()
7364
5562
  };
7365
5563
  function coerceJsonObject(v) {
@@ -7371,14 +5569,14 @@ function coerceJsonObject(v) {
7371
5569
  }
7372
5570
  }
7373
5571
  __name(coerceJsonObject, "coerceJsonObject");
7374
- var SelectorParam = z6.preprocess(
5572
+ var SelectorParam = z8.preprocess(
7375
5573
  coerceJsonObject,
7376
- z6.object(SelectorShape).refine(
5574
+ z8.object(SelectorShape).refine(
7377
5575
  (s) => Boolean(s.testId || s.text || s.label || s.pointPercent),
7378
5576
  { message: "selector requires one of testId/text/label/pointPercent" }
7379
5577
  )
7380
5578
  );
7381
- var SelectorParamOptional = z6.preprocess(coerceJsonObject, z6.object(SelectorShape).optional());
5579
+ var SelectorParamOptional = z8.preprocess(coerceJsonObject, z8.object(SelectorShape).optional());
7382
5580
 
7383
5581
  // src/mcp/tools/explore.tool.ts
7384
5582
  var ACTION_ENUM = [
@@ -7395,22 +5593,22 @@ var ACTION_ENUM = [
7395
5593
  var KEY_ENUM = ["back", "home", "enter", "escape"];
7396
5594
  var DIRECTION_ENUM = ["up", "down", "left", "right"];
7397
5595
  var stepShape = {
7398
- explorationId: z7.string().optional(),
7399
- action: z7.enum(ACTION_ENUM),
7400
- device: z7.string().optional(),
5596
+ explorationId: z9.string().optional(),
5597
+ action: z9.enum(ACTION_ENUM),
5598
+ device: z9.string().optional(),
7401
5599
  selector: SelectorParamOptional,
7402
- value: z7.string().optional(),
7403
- key: z7.enum(KEY_ENUM).optional(),
7404
- direction: z7.enum(DIRECTION_ENUM).optional(),
5600
+ value: z9.string().optional(),
5601
+ key: z9.enum(KEY_ENUM).optional(),
5602
+ direction: z9.enum(DIRECTION_ENUM).optional(),
7405
5603
  from: SelectorParamOptional,
7406
- url: z7.string().optional(),
7407
- bundleId: z7.string().optional(),
7408
- clean: z7.boolean().optional(),
7409
- timeoutMs: z7.number().int().positive().optional(),
7410
- optional: z7.boolean().optional(),
7411
- button: z7.string().optional(),
7412
- description: z7.string().optional(),
7413
- section: z7.string().optional()
5604
+ url: z9.string().optional(),
5605
+ bundleId: z9.string().optional(),
5606
+ clean: z9.boolean().optional(),
5607
+ timeoutMs: z9.number().int().positive().optional(),
5608
+ optional: z9.boolean().optional(),
5609
+ button: z9.string().optional(),
5610
+ description: z9.string().optional(),
5611
+ section: z9.string().optional()
7414
5612
  };
7415
5613
  function validateRecordingArgs(args) {
7416
5614
  if (!args.section) return "section required when recording";
@@ -7497,10 +5695,10 @@ var ExploreStartTool = class extends BaseTool {
7497
5695
  title: "Start a recording session",
7498
5696
  description: "Begins an ExplorationSession. Returns { explorationId }. Subsequent explore_step calls that include this id will be recorded into an ActionLog; calls without it are ad-hoc. Device slot ('A' or 'B') is fixed for the session. No auto-launch: the first explore_step must do the app_launch / open_deeplink so it appears in the generated test.",
7499
5697
  inputSchema: {
7500
- scenario_name: z7.string().min(1).describe("Logical name; later used by save_exploration_as_test as the test file name."),
7501
- device: z7.string().min(1).describe("Slot from SIM_POOL, e.g. 'A'."),
7502
- title: z7.string().optional(),
7503
- description: z7.string().optional()
5698
+ scenario_name: z9.string().min(1).describe("Logical name; later used by save_exploration_as_test as the test file name."),
5699
+ device: z9.string().min(1).describe("Slot from SIM_POOL, e.g. 'A'."),
5700
+ title: z9.string().optional(),
5701
+ description: z9.string().optional()
7504
5702
  }
7505
5703
  },
7506
5704
  async ({ scenario_name, device, title, description }) => this.tracked(
@@ -7536,7 +5734,7 @@ var ExploreStopTool = class extends BaseTool {
7536
5734
  title: "Stop a recording session",
7537
5735
  description: "Marks the session as stopped. The device stays running (no teardown). After stop, generate_dsl_from_exploration / save_exploration_as_test remain callable; further explore_step calls with this id will fail.",
7538
5736
  inputSchema: {
7539
- explorationId: z7.string().min(1)
5737
+ explorationId: z9.string().min(1)
7540
5738
  }
7541
5739
  },
7542
5740
  async ({ explorationId }) => this.tracked("explore_stop", { explorationId }, async () => {
@@ -7563,7 +5761,7 @@ var ExploreStateTool = class extends BaseTool {
7563
5761
  title: "Inspect the recording session log",
7564
5762
  description: "Returns the session status and all recorded ActionEntries. Each entry carries its per-entry `stability` annotation (stable | fragile). The full warnings list (FRAGILE_LOCATOR / NO_DSL_PRIMITIVE / BUNDLE_ID_IGNORED) is NOT returned here \u2014 it is derived on demand by generate_dsl_from_exploration.",
7565
5763
  inputSchema: {
7566
- explorationId: z7.string().min(1)
5764
+ explorationId: z9.string().min(1)
7567
5765
  }
7568
5766
  },
7569
5767
  async ({ explorationId }) => this.tracked("explore_state", { explorationId }, async () => {
@@ -7707,8 +5905,8 @@ var ExploreRemoveStepTool = class extends BaseTool {
7707
5905
  title: "Remove an entry from the recording session log",
7708
5906
  description: "Deletes the entry by entryId. Returns { removed: true } or { removed: false } if not found.",
7709
5907
  inputSchema: {
7710
- explorationId: z7.string().min(1),
7711
- entryId: z7.string().min(1)
5908
+ explorationId: z9.string().min(1),
5909
+ entryId: z9.string().min(1)
7712
5910
  }
7713
5911
  },
7714
5912
  async ({ explorationId, entryId }) => this.tracked("explore_remove_step", { explorationId, entryId }, async () => {
@@ -7729,9 +5927,9 @@ var GenerateDslFromExplorationTool = class extends BaseTool {
7729
5927
  "generate_dsl_from_exploration",
7730
5928
  {
7731
5929
  title: "Render the session log as a DSL test",
7732
- description: "Returns { draftDsl, warnings } from the current ActionLog. Pure function \u2014 does not mutate the log. Warnings: FRAGILE_LOCATOR (selector lacks stable identifier), NO_DSL_PRIMITIVE (entry skipped \u2014 selector shape has no DSL function), BUNDLE_ID_IGNORED (app_launch with explicit bundleId \u2014 DSL appLaunch reads it from APP_BUNDLE_ID env). Adjacent same-section entries collapse into one //@collapse \u2026 //@endcollapse block.",
5930
+ description: 'Returns { draftDsl, warnings } from the current ActionLog. Pure function \u2014 does not mutate the log. Warnings: FRAGILE_LOCATOR (selector lacks stable identifier), NO_DSL_PRIMITIVE (entry skipped \u2014 selector shape has no DSL function), BUNDLE_ID_IGNORED (app_launch with explicit bundleId \u2014 DSL appLaunch reads it from APP_BUNDLE_ID env). Adjacent same-section entries group into one step("section", () => { ... }) block - the mandatory test_* body structure.',
7733
5931
  inputSchema: {
7734
- explorationId: z7.string().min(1)
5932
+ explorationId: z9.string().min(1)
7735
5933
  }
7736
5934
  },
7737
5935
  async ({ explorationId }) => this.tracked("generate_dsl_from_exploration", { explorationId }, async () => {
@@ -7753,12 +5951,12 @@ var SaveExplorationAsTestTool = class extends BaseTool {
7753
5951
  "save_exploration_as_test",
7754
5952
  {
7755
5953
  title: "Persist the generated DSL as a runnable scenario",
7756
- description: "Renders the ActionLog as DSL and writes it to unotest/e2e/<scenarioName>.js. Returns { path, actionCount, warnings }. Policy: by default, NO_DSL_PRIMITIVE warnings block the save \u2014 pass force: true to write anyway with `// SKIPPED \u2026` comments inline. If the file already exists, pass overwrite: true to replace it.",
5954
+ description: "Renders the ActionLog as DSL and writes it to unotest/e2e-mobile/<scenarioName>.js. Returns { path, actionCount, warnings }. Policy: by default, NO_DSL_PRIMITIVE warnings block the save \u2014 pass force: true to write anyway with `// SKIPPED \u2026` comments inline. If the file already exists, pass overwrite: true to replace it.",
7757
5955
  inputSchema: {
7758
- explorationId: z7.string().min(1),
7759
- scenarioName: z7.string().min(1),
7760
- overwrite: z7.boolean().optional(),
7761
- force: z7.boolean().optional()
5956
+ explorationId: z9.string().min(1),
5957
+ scenarioName: z9.string().min(1),
5958
+ overwrite: z9.boolean().optional(),
5959
+ force: z9.boolean().optional()
7762
5960
  }
7763
5961
  },
7764
5962
  async ({ explorationId, scenarioName, overwrite, force }) => this.tracked(
@@ -7847,6 +6045,17 @@ function registerAllTools(server, services) {
7847
6045
  new InspectRuntimeTool(dbgServices).register(server);
7848
6046
  new AbortRuntimeTool(dbgServices).register(server);
7849
6047
  new ListRuntimesTool(dbgServices).register(server);
6048
+ const attachState = new AttachState();
6049
+ const attachSlice = { sessionRecorder: services.sessionRecorder };
6050
+ const projectRoot = process.cwd();
6051
+ new GetActiveContextTool(attachSlice, attachState, projectRoot).register(server);
6052
+ new AttachDebugSessionTool(attachSlice, attachState, projectRoot).register(server);
6053
+ new DetachDebugSessionTool(attachSlice, attachState, projectRoot).register(server);
6054
+ new WheelTool(attachSlice, attachState, projectRoot, "agent").register(server);
6055
+ new WheelTool(attachSlice, attachState, projectRoot, "human").register(server);
6056
+ new ListFailuresTool(attachSlice, projectRoot).register(server);
6057
+ new GetFailureScreenshotTool(attachSlice, projectRoot).register(server);
6058
+ new GetFailureA11yTool(attachSlice, projectRoot).register(server);
7850
6059
  new AppInstallTool({
7851
6060
  envConfig: services.envConfig,
7852
6061
  logger: services.logger,