diff --git a/pkg/spec/src/actions.ts b/pkg/spec/src/actions.ts index 7ced33e..2b85eea 100644 --- a/pkg/spec/src/actions.ts +++ b/pkg/spec/src/actions.ts @@ -1,7 +1,13 @@ +// Author-facing action factories. Each returns plain GeneratorNode / +// ActionDescriptor DATA (see action-tree.ts); the shared picker (pick.ts) walks +// the tree and the runtime entry (runtime-entry.ts) serializes the result. No +// factory forwards to globalThis.__sanderling__ anymore: the same data tree +// drives both the goja verifier and the V8 web runtime. + +import type { Pcg } from "./pcg.ts"; import type { AccessibilityElement, Action, - ActionGenerator, Direction, DoubleTapAction, InputTextAction, @@ -16,16 +22,17 @@ import type { WaitAction, WeightedEntry, } from "./types.ts"; +import type { ActionDescriptor, GeneratorNode } from "./action-tree.ts"; -export function actions(generator: () => Action[]): ActionGenerator { - return globalThis.__sanderling__.actions(generator); +export function actions(generator: () => Action[]): GeneratorNode { + return { kind: "actions", generate: generator as () => ActionDescriptor[] }; } export function whenRoute( routeExtractor: { readonly current: string | null }, routes: string | readonly string[], body: () => Action[], -): ActionGenerator { +): GeneratorNode { const allowed = typeof routes === "string" ? [routes] : routes; return actions(() => { const current = routeExtractor.current; @@ -34,38 +41,54 @@ export function whenRoute( }); } -export function weighted(...entries: WeightedEntry[]): ActionGenerator { - return globalThis.__sanderling__.weighted(...entries); +export function weighted(...entries: WeightedEntry[]): GeneratorNode { + return { kind: "weighted", branches: entries }; +} + +// samplerRng is the picker's Pcg while it evaluates an `actions` node's +// generator (set by pick.ts walkActions). `from(...).generate()` draws from it +// so sampling shares the single deterministic stream. It is null outside a +// walk; eager spec-time generate() calls then fall back to the first item. +let samplerRng: Pcg | null = null; + +export function setSamplerRng(rng: Pcg | null): void { + samplerRng = rng; } export function from(items: readonly T[]): Sampler { - return globalThis.__sanderling__.from(items); + return { + generate(): T { + if (items.length <= 1) return items[0] as T; + const index = samplerRng ? samplerRng.intN(items.length) : 0; + return items[index] as T; + }, + }; } export function Tap(parameters: { on: string | AccessibilityElement }): TapAction { - return globalThis.__sanderling__.tap(parameters); + return { kind: "Tap", on: parameters.on }; } export function DoubleTap(parameters: { on: string | AccessibilityElement }): DoubleTapAction { - return globalThis.__sanderling__.doubleTap(parameters); + return { kind: "DoubleTap", on: parameters.on }; } export function LongPress(parameters: { on: string | AccessibilityElement }): LongPressAction { - return globalThis.__sanderling__.longPress(parameters); + return { kind: "LongPress", on: parameters.on }; } export function Scroll(parameters: { direction: Direction; in?: string | AccessibilityElement; }): ScrollAction { - return globalThis.__sanderling__.scroll(parameters); + return { kind: "Scroll", direction: parameters.direction, in: parameters.in }; } export function InputText(parameters: { into: string | AccessibilityElement; text: string; }): InputTextAction { - return globalThis.__sanderling__.inputText(parameters); + return { kind: "InputText", into: parameters.into, text: parameters.text }; } export function Swipe(parameters: { @@ -73,44 +96,27 @@ export function Swipe(parameters: { to: Point | AccessibilityElement; durationMillis?: number; }): SwipeAction { - return globalThis.__sanderling__.swipe(parameters); + return { + kind: "Swipe", + from: parameters.from, + to: parameters.to, + durationMillis: parameters.durationMillis, + }; } export function PressKey(parameters: { key: Key }): PressKeyAction { - return globalThis.__sanderling__.pressKey(parameters); + return { kind: "PressKey", key: parameters.key }; } export function Wait(parameters: { durationMillis: number }): WaitAction { - return globalThis.__sanderling__.wait(parameters); + return { kind: "Wait", durationMillis: parameters.durationMillis }; } -function builtinGenerator( - name: - | "taps" - | "doubleTaps" - | "longPresses" - | "scrolls" - | "typing" - | "swipes" - | "waitOnce" - | "pressKeys", -): ActionGenerator { - return new Proxy({} as ActionGenerator, { - get(_target, property) { - const runtime = globalThis.__sanderling__[name] as unknown as Record< - string | symbol, - unknown - >; - return runtime[property]; - }, - }); -} - -export const taps: ActionGenerator = builtinGenerator("taps"); -export const doubleTaps: ActionGenerator = builtinGenerator("doubleTaps"); -export const longPresses: ActionGenerator = builtinGenerator("longPresses"); -export const scrolls: ActionGenerator = builtinGenerator("scrolls"); -export const typing: ActionGenerator = builtinGenerator("typing"); -export const swipes: ActionGenerator = builtinGenerator("swipes"); -export const waitOnce: ActionGenerator = builtinGenerator("waitOnce"); -export const pressKey: ActionGenerator = builtinGenerator("pressKeys"); +export const taps: GeneratorNode = { kind: "builtin", verb: "taps" }; +export const doubleTaps: GeneratorNode = { kind: "builtin", verb: "doubleTaps" }; +export const longPresses: GeneratorNode = { kind: "builtin", verb: "longPresses" }; +export const scrolls: GeneratorNode = { kind: "builtin", verb: "scrolls" }; +export const typing: GeneratorNode = { kind: "builtin", verb: "typing" }; +export const swipes: GeneratorNode = { kind: "builtin", verb: "swipes" }; +export const waitOnce: GeneratorNode = { kind: "builtin", verb: "waitOnce" }; +export const pressKey: GeneratorNode = { kind: "builtin", verb: "pressKeys" }; diff --git a/pkg/spec/src/types.ts b/pkg/spec/src/types.ts index 384be42..c556e0e 100644 --- a/pkg/spec/src/types.ts +++ b/pkg/spec/src/types.ts @@ -175,10 +175,10 @@ export type Key = | "left" | "right"; -export interface ActionGenerator { - readonly __sanderlingActionGenerator: true; - generate(): Action[]; -} +// ActionGenerator is a node in the action-generator tree (see action-tree.ts). +// Author specs treat it as an opaque handle they compose with weighted(); the +// shared picker walks the underlying GeneratorNode. +export type ActionGenerator = GeneratorNode; export interface Formula { readonly __sanderlingFormula: true; @@ -196,45 +196,21 @@ export interface Sampler { generate(): T; } +// SanderlingRuntime is the host-bound surface that stays on +// globalThis.__sanderling__: extract plus the LTL formula constructors. Action +// factories no longer live here; they return plain data trees (see actions.ts). export interface SanderlingRuntime { extract: (getter: (state: State) => T, name?: string) => Extracted; always: (predicateOrFormula: (() => boolean) | Formula) => Formula; now: (predicate: () => boolean) => Formula; next: (predicate: () => boolean) => Formula; eventually: (predicate: () => boolean) => EventuallyFormula; - actions: (generator: () => Action[]) => ActionGenerator; - weighted: (...entries: WeightedEntry[]) => ActionGenerator; - from: (items: readonly T[]) => Sampler; - tap: (parameters: { on: string | AccessibilityElement }) => TapAction; - doubleTap: (parameters: { on: string | AccessibilityElement }) => DoubleTapAction; - longPress: (parameters: { on: string | AccessibilityElement }) => LongPressAction; - scroll: (parameters: { - direction: Direction; - in?: string | AccessibilityElement; - }) => ScrollAction; - inputText: (parameters: { - into: string | AccessibilityElement; - text: string; - }) => InputTextAction; - swipe: (parameters: { - from: Point | AccessibilityElement; - to: Point | AccessibilityElement; - durationMillis?: number; - }) => SwipeAction; - pressKey: (parameters: { key: Key }) => PressKeyAction; - wait: (parameters: { durationMillis: number }) => WaitAction; - taps: ActionGenerator; - doubleTaps: ActionGenerator; - longPresses: ActionGenerator; - scrolls: ActionGenerator; - typing: ActionGenerator; - swipes: ActionGenerator; - waitOnce: ActionGenerator; - pressKeys: ActionGenerator; } export type WeightedEntry = readonly [number, ActionGenerator]; +import type { GeneratorNode } from "./action-tree.ts"; + declare global { // eslint-disable-next-line no-var var __sanderling__: SanderlingRuntime;