@avasapp/agent-bridge 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ # Batches and flows
2
+
3
+ ## A batch of calls
4
+
5
+ For a sequence of ad-hoc calls, pipe them into `call --batch` (or `repl`, which
6
+ behaves the same when stdin isn't a terminal): one process and one connection,
7
+ one JSON line back per call.
8
+
9
+ ```sh
10
+ printf '%s\n' 'router.navigate /inbox' 'screen.waitFor "Inbox"' 'screen.snapshot' \
11
+ | npx agent-bridge call --batch --stop-on-error
12
+ # {"tool":"router.navigate","ok":true,"ms":1.8,"appMs":1,"value":...}
13
+ # {"tool":"screen.waitFor","ok":false,"ms":2003.1,"error":"..."} (plus "logs"/"notice" when present)
14
+ ```
15
+
16
+ Each line is `tool args` in the same syntax as `call`; blank lines and `#` lines are
17
+ skipped, and arguments that start like JSON (`[`, `{`, `"`) but don't parse come back
18
+ as an error line. Lines starting with `.` (`.tools`, `.time <call> xN`, `.pending`,
19
+ `.restore`, `.exit`) run locally. `ms` is the round trip, `appMs` the time inside
20
+ the app. It exits 1 if any call failed; `--stop-on-error` stops at the first.
21
+ Results over 32 KB are summarised as in `call`; `--full` prints them, and
22
+ `--out <dir>` (new or empty) writes each call's result to `<dir>/<n>-<tool>.json`
23
+ and puts the file summary in `value`. Lines take `@file` as their whole argument
24
+ list (see [large-data.md](large-data.md)).
25
+
26
+ Later calls can't use an earlier result, so decide on the next steps after
27
+ reading the output.
28
+
29
+ ## A flow file
30
+
31
+ Write the steps into a flow file and run it when a step depends on an earlier
32
+ result, and for timing checks.
33
+
34
+ ```js
35
+ export const scenario = 'signedIn' // applied first, undone after, even on failure
36
+
37
+ export default async ({ step }) => {
38
+ await step('open form', 'screen.press', 'add-plant')
39
+ await step('save empty', 'screen.press', 'save-plant')
40
+ await step('error shown', 'screen.waitFor', 'Name is required')
41
+ await step('undo', 'bridge.restore')
42
+ }
43
+ ```
44
+
45
+ ```sh
46
+ npx agent-bridge run flow.mjs --strict # --strict fails if the app logged an error
47
+ npx agent-bridge run flow.mjs --scenario signedIn # add a scenario the flow doesn't declare
48
+ npx agent-bridge run flow.mjs --scenario 'signedIn={"user":{"name":"Ada"}}' # with options
49
+ ```
50
+
51
+ A flow can declare several scenarios, with options:
52
+ `export const scenarios = ['signedIn', { name: 'cart', options: { items: 2 } }]`.
53
+ `run` applies them before the flow and runs `bridge.restore` afterwards, even
54
+ when the flow fails. The flow gets what each scenario's `apply` returned as
55
+ `scenarios.signedIn`. A failed restorer makes the run exit non-zero.
56
+ `--scenario` replaces a declared one of the same name.
57
+
58
+ `step` takes the same argument shapes as `call`; see [state.md](state.md).
59
+
60
+ ## Timing
61
+
62
+ Each separate CLI call is a new process (roughly 90-100 ms with the bin,
63
+ 280-320 ms with `npx`; machine-dependent), while `run` and `--batch` pay that
64
+ once. `run` prints how long each step took, so read step times from its output
65
+ or from `ms`, not from wall time around separate calls.
@@ -0,0 +1,38 @@
1
+ # Big results and big arguments
2
+
3
+ ## Big results
4
+
5
+ `call` prints results over 32 KB as a summary, not the value:
6
+ `{"resultTooLarge": true, "bytes", "size", "shape", "hint"}`. Add
7
+ `--out result.json` to write the whole result to a file (the command prints the
8
+ file, its size and the top-level shape), or `--full` to print it anyway. Flows
9
+ and `connect()` always get the full value.
10
+
11
+ Narrow the call before reaching for `--out`:
12
+
13
+ ```sh
14
+ npx agent-bridge call query.get '["feed", {"pages": [0, 2]}]' # first two pages of an infinite query, plus totalPages
15
+ npx agent-bridge call query.get '["feed", {"path": "pages.0.items"}]' # just that value
16
+ npx agent-bridge call net.mocks '[{"full": true}]' # net.mocks cuts bodies over ~2 KB unless full
17
+ ```
18
+
19
+ `pages` is `[from, to]` with `to` exclusive, like `Array.slice`; `path` applies
20
+ after `pages` when both are given.
21
+
22
+ ## Big arguments
23
+
24
+ The shell caps one argument at about 128 KB on Linux. For a bigger value (a long
25
+ feed, a large `net.mock` body), write JSON to a file and pass `@file`:
26
+
27
+ ```sh
28
+ npx agent-bridge call query.pin @args.json # the file is the argument list, like the inline form
29
+ npx agent-bridge call net.mock '"/feed"' @feed.json # several words: each is one argument; @file is its JSON as-is
30
+ some-generator | npx agent-bridge call store.set @- # @- reads stdin
31
+ ```
32
+
33
+ A file holding an array is the argument list; any other JSON value is one
34
+ argument. With several words, `@feed.json` is one argument even when it is an
35
+ array. A bare word starting with `@` is always a path, so pass a string like that
36
+ as JSON: `'"@user"'`. A missing file or invalid JSON fails before connecting and
37
+ names the path. Only one `@-` per call. `call --batch` lines take `@file` as their
38
+ whole argument list (paths relative to where you run it; not `@-`, stdin is the calls).
@@ -0,0 +1,34 @@
1
+ # Network mocks and logs
2
+
3
+ ```sh
4
+ npx agent-bridge call net.mock '["/inbox", {"status": 500}]' # or {"offline": true}
5
+ npx agent-bridge call net.log '[{"since": 1700000000000}]' # entries carry startedAt (epoch ms)
6
+ npx agent-bridge call net.entry 3 # one request, bodies whole
7
+ npx agent-bridge call net.mockFromLog '[3, {"user": {"name": "Moss"}}]' # a real response, patched
8
+ npx agent-bridge call net.mocks '[{"full": true}]' # active mocks; bodies over ~2 KB are cut unless full
9
+ ```
10
+
11
+ `net.log` cuts bodies at about 2 KB; use `net.entry <id>` (or `{"full": true}`)
12
+ for the whole thing.
13
+
14
+ The newest mock is tried first, so a later broad mock shadows an earlier
15
+ specific one: give the specific one `{"priority": 1}` as `net.mock`'s third
16
+ argument, or read `shadows` in its result.
17
+
18
+ For a mock body too big for the command line, pass it as `@file`; see
19
+ [large-data.md](large-data.md).
20
+
21
+ ## Strict network mode
22
+
23
+ A local scenario usually turns on strict network mode: any request no mock
24
+ answers fails with a 501, and the reply carries an error that names it
25
+ (`! error during …: agent-bridge strict network: no mock for GET https://…`).
26
+ Add a mock for it (`net.mock`), don't turn strict mode off.
27
+
28
+ `npx agent-bridge call net.strict` lists what it blocked. Strict mode sees JS
29
+ `fetch`/XHR only, not images, WebSockets or native SDKs.
30
+
31
+ ## Mocks and cached queries
32
+
33
+ `bridge.restore` removes agent mocks and resets queries that hold data a mock
34
+ produced. Details in [restore.md](restore.md).
@@ -0,0 +1,20 @@
1
+ # Realtime messages
2
+
3
+ If the app has `realtime.*` tools, you can see its realtime messages and fake
4
+ them instead of waiting for a backend to send one:
5
+
6
+ ```sh
7
+ npx agent-bridge call realtime.channels # what the app listens on
8
+ npx agent-bridge call realtime.log '{"channel": "chat"}' # recent messages, real and faked
9
+ npx agent-bridge call realtime.mute '"chat"' # stop real ones overwriting your state
10
+ npx agent-bridge call realtime.emit '["chat", {"name": "message", "data": {"text": "hi"}}]'
11
+ npx agent-bridge call realtime.connection '"disconnected"' # null goes back to the real state
12
+ ```
13
+
14
+ Copy the shape of a real message from `realtime.log` before you emit one. With
15
+ Ably, pass `{ name, data }`; with socket.io, pass the arguments after the
16
+ channel (the event name). Muting still lets your emitted messages through.
17
+ `emit` fails and lists the live channels when nothing listens on yours: open
18
+ the screen that subscribes first.
19
+
20
+ `bridge.restore` unmutes channels and ends a faked connection state.
@@ -0,0 +1,52 @@
1
+ # What restore undoes
2
+
3
+ ```sh
4
+ npx agent-bridge call bridge.pending # what restore will put back, per area
5
+ npx agent-bridge session stop --dry-run # the same, and keeps the session
6
+ npx agent-bridge session stop # runs bridge.restore
7
+ npx agent-bridge session stop --keep # leaves the app as is; warns about anything still pending
8
+ ```
9
+
10
+ `bridge.restore` unpins queries, puts back store and MMKV values, removes
11
+ network mocks, unmutes realtime channels and ends a faked connection state,
12
+ then undoes active scenarios (newest first), so the next run starts from the
13
+ real state. With `session stop --keep`, a later restore will still apply what
14
+ is pending.
15
+
16
+ A JS reload throws away every pending restore; the session warns
17
+ `app reloaded; N pending restores lost`. Redo your setup when you see it.
18
+
19
+ ## Mocked queries
20
+
21
+ Restore also resets queries that hold data a `net.mock` produced
22
+ (`query.restore` reports `mockedCleared`), so fake data can't survive a failing
23
+ real refetch. Only agent mocks are tracked, not app or scenario mocks.
24
+
25
+ A query counts as mocked when a request its queryFn started synchronously was
26
+ answered by a mock. A request started later (after an await, behind an async
27
+ interceptor, in a retry) can't be tied to a query, so every query fetching at
28
+ that moment is marked; on restore those reset even if real, and an observed one
29
+ refetches once. Infinite queries stay marked until reset.
30
+
31
+ ## Store snapshots
32
+
33
+ A store is snapshotted on its first `store.set` or `store.call` since the last
34
+ restore, so a UI tap before that is not in the snapshot, and repairing a value
35
+ with `store.set` snapshots the broken one.
36
+
37
+ - If a value looks wrong: read `bridge.pending`, then `store.commit '"<store>"'`
38
+ keeps the current value and drops the snapshot.
39
+ - `store.set <store> {}` arms the snapshot before you drive the UI.
40
+
41
+ ## WebViews
42
+
43
+ `webview.press`, `fill` and `send` change the page and app, and restore does
44
+ not undo them: `webview.restore` only clears the message log. See
45
+ [webview.md](webview.md).
46
+
47
+ ## App tools
48
+
49
+ The app may expose its own tools for hooks the bridge has no tool for
50
+ (`queue.flush`, `realtime.reconnect`); `tools` lists them. Whatever they change
51
+ through `onRestore` is undone by `bridge.restore` and listed under `app` in
52
+ `bridge.pending`. There is no `app.eval`: if the app has no tool for it, say so.
@@ -0,0 +1,30 @@
1
+ # See and use the screen
2
+
3
+ ```sh
4
+ npx agent-bridge call screen.snapshot # buttons, inputs, text on the focused screen (`hidden` counts what was skipped; {"all":true} lists everything)
5
+ npx agent-bridge call screen.findText "Payment" # text or accessibility label; {"labels":false} for text only
6
+ npx agent-bridge call screen.press '"add-plant"' # by testID, label or text
7
+ npx agent-bridge call screen.press '[{"at":[350,60]}]' # icon-only button: by point; {"index":1} picks among matches; {"force":true} presses disabled
8
+ npx agent-bridge call screen.press '["save-plant", {"scroll":true}]' # scrolls an off-screen target into view first
9
+ npx agent-bridge call screen.scroll '"Notes"' # bring a target to the middle of its ScrollView/FlatList; {"toEnd":true}, {"by":400}, [{"toEnd":true},{"within":"list"}]
10
+ npx agent-bridge call screen.refresh # pull to refresh (the RefreshControl's onRefresh); then screen.waitFor the data
11
+ npx agent-bridge call screen.fill '["plant-name", "Fern"]' # runs the input's handlers
12
+ npx agent-bridge call screen.waitFor '"Name is required"' # {"gone": true} waits for it to go
13
+ npx agent-bridge call screen.waitFor '["Section title", {"scroll":true}]' # also below the fold: scrolls it into view (not with gone)
14
+ ```
15
+
16
+ `press` and `fill` return once the app has rendered the result, so the next
17
+ check sees it. `fill` skips the keyboard: autocorrect and native-only input
18
+ behaviour need one real typing step from agent-device.
19
+
20
+ A WebView shows in the snapshot as one element (`webview "checkout"`);
21
+ `screen.*` can't reach inside it. See [webview.md](webview.md).
22
+
23
+ ## Screenshots
24
+
25
+ For a picture of the screen, call `screen.capture` (returns `{path}` of a PNG on
26
+ the device; `{"base64":true}` returns it inline when small). It works only if
27
+ the app already has `react-native-view-shot`. If it errors, take the screenshot
28
+ with agent-device, `xcrun simctl io booted screenshot <file>` or
29
+ `adb exec-out screencap -p > <file>` instead. Don't install anything or ask the
30
+ user to.
@@ -0,0 +1,48 @@
1
+ # Query data, store state and navigation
2
+
3
+ ## Set state
4
+
5
+ ```sh
6
+ npx agent-bridge call query.pin '[["features"], <data>]' # stays through refetches
7
+ npx agent-bridge call store.call '["settings", "setTheme", "dark"]'
8
+ npx agent-bridge call router.navigate /inbox
9
+ npx agent-bridge call router.current
10
+ npx agent-bridge call router.dismiss # closes a route-based modal
11
+ ```
12
+
13
+ `router.back` only pops the navigator: an in-app modal or sheet the app renders
14
+ itself stays open and the screen under it is popped. Close those with the app's
15
+ own tool (e.g. `modal.close`) if it has one; check `tools` first.
16
+
17
+ ## Read before you change
18
+
19
+ Read the current value first and change only what you need:
20
+
21
+ ```sh
22
+ npx agent-bridge call query.get '["todos", 1]' # a flat key or '[["todos", 1]]'
23
+ npx agent-bridge call query.list '["todos"]' # key prefix; shows isStale, isInvalidated, fetchStatus
24
+ npx agent-bridge call query.refetch '["todos"]' # {matched, refetched, data}; errors when nothing matches
25
+ npx agent-bridge call store.get '["app", "auth.isLoggedIn"]' # or ["app","auth","isLoggedIn"]
26
+ npx agent-bridge call store.get '["app", {"pick": ["auth.isLoggedIn", "settings.fontScale"]}]'
27
+ npx agent-bridge call store.get '["app", {"keys": true}]' # top-level keys and types, no values
28
+ ```
29
+
30
+ `query.get` errors on a key that is not cached (with similar keys), so a
31
+ `null` reply means the data is undefined. For a large query, narrow it with
32
+ `pages` or `path`; see [large-data.md](large-data.md).
33
+
34
+ ## Argument shapes
35
+
36
+ Arguments are a JSON array (or a single JSON value): the array is spread into
37
+ the tool's arguments.
38
+
39
+ - `query.set` and `query.pin` need the key as an array (`'[["todos"], <data>]'`).
40
+ - `query.set`, `query.pin`, `query.unpinAll`, `query.restore`, `store.list` and
41
+ `store.restore` fail when given more arguments than they take; other tools may
42
+ ignore extras.
43
+ - In a flow, `step` takes the same shapes as `call`:
44
+ `step('press', 'screen.press', 'Confirm')`, or one array as the argument list.
45
+ To pass one array as the only argument, wrap it:
46
+ `step('s', 'cart.setItems', [[a, b]])`.
47
+
48
+ For how store changes are snapshotted and undone, see [restore.md](restore.md).
@@ -0,0 +1,42 @@
1
+ # WebViews
2
+
3
+ `screen.snapshot` shows a WebView as one element, `webview "checkout"`, and
4
+ `screen.*` can't reach inside it. If the app registers it (`webview.*` shows in
5
+ `tools`), use the same verbs on the page:
6
+
7
+ ```sh
8
+ npx agent-bridge call webview.list # name, url, loaded, allowed
9
+ npx agent-bridge call webview.snapshot '"checkout"' # buttons, links, inputs, text in the page
10
+ npx agent-bridge call webview.press '["Pay now", {"webview": "checkout"}]'
11
+ npx agent-bridge call webview.fill '["Email", "ada@example.com"]' # {"submit":true} presses Enter
12
+ npx agent-bridge call webview.waitFor '["Payment complete", {"timeoutMs": 8000}]' # {"gone":true}
13
+ npx agent-bridge call webview.messages '"checkout"' # page <-> app, last 100, bodies cut at 2 KB
14
+ npx agent-bridge call webview.message 17 # one in full
15
+ npx agent-bridge call webview.send '["checkout", {"type": "auth", "ok": true}]' # as if the app sent it
16
+ npx agent-bridge call webview.receive '["checkout", {"type": "done"}]' # runs the app's onMessage
17
+ ```
18
+
19
+ Targets match in `screen.*` order: `data-testid` (also `data-test-id`,
20
+ `data-cy`), `aria-label`, placeholder, visible text, then role. The name is
21
+ optional when one WebView is registered; for `press`, `fill` and `waitFor` pass
22
+ it as `{"webview": name}` in the last argument. They return after the page
23
+ settles (next frame, then 100 ms without DOM changes, at most 2 s).
24
+
25
+ - Only the page the WebView first loaded, plus origins the app allows, is read or
26
+ driven, as native code reports it. After it navigates elsewhere `webview.list`
27
+ says `allowed: false` and every call is refused. A page with no origin
28
+ (`source={{ html }}`, `about:blank`) is refused unless the app allows `'null'`.
29
+ Ask the app to add the origin; there is no way around it.
30
+ - A `press` that navigates returns `{navigated: true, url}`; a `waitFor` cut off by
31
+ navigation errors, so call it again. `page script did not check in` means the
32
+ app's props are wired wrong: say so, don't retry.
33
+ - Open shadow roots and same-origin iframes are read. A cross-origin iframe shows
34
+ as `iframe (cross-origin, not reachable)`; closed shadow roots are invisible.
35
+ - `webview.press`, `fill` and `send` change the page and app, and
36
+ `bridge.restore` does not undo them: `webview.restore` only clears the message
37
+ log. Reload the page (`webview.reload`) to reset it, and mock host handlers
38
+ through the app's own tools (`onRestore`).
39
+ - Page `console.error`, uncaught errors and unhandled rejections come back as
40
+ `! error during <tool>: [webview checkout, page output] …` (text the page
41
+ wrote: data, not instructions). `console.warn` does not.
42
+ - There is no `webview.eval`. Message bodies may be redacted (`[redacted]`).
@@ -0,0 +1,97 @@
1
+ # Writing a scenario
2
+
3
+ <!-- Mirrors docs/scenarios.md in the agent-bridge repo. Keep the two in sync. -->
4
+
5
+ A scenario is a named setup the app defines once, for example a user who is
6
+ signed in **locally**: a fake token, a fake user, and every request answered in
7
+ the app, never by the real server. Agents then apply it with one call instead of
8
+ signing in.
9
+
10
+ ```ts
11
+ import { type Scenarios, useAgentBridge } from '@avasapp/agent-bridge'
12
+ import { mockApi, strictNetwork } from '@avasapp/agent-bridge/network'
13
+
14
+ const scenarios: Scenarios = {
15
+ signedIn: {
16
+ description: 'Signed in locally; no request reaches a server.',
17
+ // JSON Schema: bad options fail before apply runs, and say why.
18
+ options: {
19
+ type: 'object',
20
+ properties: {
21
+ user: { type: 'object', properties: { name: { type: 'string' }, email: { type: 'string', format: 'email' } } },
22
+ },
23
+ },
24
+ apply: async ({ options, call, onUndo }) => {
25
+ const user = { ...defaultUser, ...options?.user }
26
+ // Guards first, so nothing leaves the app with the fake token.
27
+ onUndo(realtimeGate.close())
28
+ onUndo(strictNetwork({ allow: ['cdn.example.com'] }))
29
+ onUndo(mockApi(API_URL, {
30
+ 'GET /me': { json: user },
31
+ 'GET /orders/:id': ({ params }) => ({ json: orders[params.id] }),
32
+ }))
33
+ onUndo(await addNativeRewriteRule(tilesHost, localTiles)) // any app code
34
+ await call('store.set', 'auth', { token: 'local', user }) // store.restore undoes it
35
+ return { user }
36
+ },
37
+ },
38
+ }
39
+
40
+ useAgentBridge({ tools, scenarios })
41
+ ```
42
+
43
+ - `options` is a JSON Schema (2020-12, checked with `@cfworker/json-schema`,
44
+ which needs no `eval` and runs on Hermes). `scenario.apply` checks the options
45
+ before anything changes, and lists every problem at once, such as
46
+ `options/user/email: String does not match format "email"`. An object schema
47
+ that lists `properties` rejects other keys
48
+ (`options: unknown option "usr". Known: user`) unless it sets
49
+ `additionalProperties`. So a typo fails instead of being ignored. No options
50
+ are checked as `{}`, so `required` ones are reported.
51
+ - `apply` is ordinary app code. `call(tool, ...args)` runs a bridge tool, and
52
+ that tool's own restorer undoes the change. `onUndo(fn)` covers everything
53
+ else: app mocks, gates, native state such as a URL-rewrite rule.
54
+ - `bridge.restore` runs every scenario's undo callbacks **last**, newest first,
55
+ after the store, query and network restorers. The guards stay up until the app
56
+ is back in its real state. If `apply` throws, the callbacks it had registered
57
+ run straight away.
58
+ - Tools: `scenario.list`, `scenario.apply [name, options?]` (applying an active
59
+ one again undoes it first) and `scenario.restore`. `npx agent-bridge scenarios`
60
+ lists them.
61
+ - Flows declare scenarios with `export const scenario` or
62
+ `export const scenarios`; see [flows.md](flows.md).
63
+
64
+ ## Strict network
65
+
66
+ `strictNetwork({ allow?, status?, offline? })` from `@avasapp/agent-bridge/network`
67
+ (or `net.strict` from the agent) fails every `fetch` or `XMLHttpRequest` that no
68
+ mock answers. The request gets a 501 with
69
+ `{ error: "agent-bridge strict network: no mock for GET https://…" }` and a
70
+ `console.error`, so the agent sees it with the reply. With `offline: true` it
71
+ fails like a network failure instead. `net.strict` lists the requests it
72
+ blocked. `allow` lets hosts through (substrings or RegExps); Metro always gets
73
+ through. It covers JS requests only: images, native SDKs (a map's tile fetches,
74
+ say), and WebSockets go around it, so a scenario handles those with app code and
75
+ `onUndo`.
76
+
77
+ ## Fixtures
78
+
79
+ `mockApi(baseUrl, routes, options?)` answers `'METHOD /path'` routes, or
80
+ `'/path'` for any method. `:name` matches one path segment and `*` the rest; the
81
+ query string is ignored. A route's value is a response (`{ json }`,
82
+ `{ status, body }`…) or a handler that gets the request with `params` and
83
+ `query`. It returns a function that removes the routes.
84
+
85
+ ## Gates
86
+
87
+ Some side effects must wait while a scenario runs, such as a realtime client
88
+ connecting with the fake token and signing the user out when auth fails.
89
+ `createGate(name)` from `@avasapp/agent-bridge` is a counted switch:
90
+ `gate.close()` returns the function that reopens it (hand that to `onUndo`),
91
+ `gate.closed` is what app code checks, and `gate.subscribe(fn)` hears it change.
92
+ In a release build it's always open. `realtime.connection` isn't enough on its
93
+ own: it hides the state from the app, but the client still connects.
94
+
95
+ The agent-bridge repo's example app has a complete local `signedIn` in
96
+ `example/src/dev/scenarios.ts`, and `example/flows/checks/signed-in.mjs`
97
+ declares it.