@avasapp/agent-bridge 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/dist/adapters/ably.cjs +79 -3
- package/dist/adapters/ably.d.cts +2 -1
- package/dist/adapters/async-storage.cjs +158 -0
- package/dist/adapters/async-storage.d.cts +14 -0
- package/dist/adapters/device.cjs +1110 -0
- package/dist/adapters/device.d.cts +49 -0
- package/dist/adapters/expo-router.cjs +112 -6
- package/dist/adapters/expo-router.d.cts +1 -1
- package/dist/adapters/jotai.cjs +266 -0
- package/dist/adapters/jotai.d.cts +19 -0
- package/dist/adapters/react-native-mmkv.cjs +80 -0
- package/dist/adapters/react-native-mmkv.d.cts +3 -1
- package/dist/adapters/react-native-webview.d.cts +1 -1
- package/dist/adapters/react-navigation.cjs +293 -0
- package/dist/adapters/react-navigation.d.cts +55 -0
- package/dist/adapters/redux.cjs +322 -0
- package/dist/adapters/redux.d.cts +28 -0
- package/dist/adapters/secure-store.cjs +169 -0
- package/dist/adapters/secure-store.d.cts +28 -0
- package/dist/adapters/socket-io.cjs +77 -3
- package/dist/adapters/socket-io.d.cts +2 -1
- package/dist/adapters/tanstack-query.d.cts +1 -1
- package/dist/adapters/zustand.cjs +193 -22
- package/dist/adapters/zustand.d.cts +11 -15
- package/dist/{chunk-3QIG7A5W.js → chunk-Y6QFU5ON.js} +233 -26
- package/dist/cli.js +1336 -255
- package/dist/client/index.cjs +233 -27
- package/dist/client/index.d.ts +67 -4
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +320 -73
- package/dist/network/index.d.cts +18 -3
- package/dist/noop/async-storage.cjs +26 -0
- package/dist/noop/device.cjs +26 -0
- package/dist/noop/jotai.cjs +26 -0
- package/dist/noop/react-navigation.cjs +26 -0
- package/dist/noop/realtime.cjs +6 -1
- package/dist/noop/redux.cjs +28 -0
- package/dist/noop/secure-store.cjs +26 -0
- package/dist/realtime/index.cjs +71 -4
- package/dist/realtime/index.d.cts +21 -2
- package/dist/runtime/index.cjs +785 -128
- package/dist/runtime/index.d.cts +2 -2
- package/dist/store-common-j0cHHvBr.d.cts +23 -0
- package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
- package/entries/async-storage.cjs +9 -0
- package/entries/device.cjs +9 -0
- package/entries/jotai.cjs +9 -0
- package/entries/react-navigation.cjs +9 -0
- package/entries/redux.cjs +9 -0
- package/entries/secure-store.cjs +9 -0
- package/package.json +32 -1
- package/skills/agent-bridge/SKILL.md +39 -8
- package/skills/agent-bridge/references/device.md +21 -0
- package/skills/agent-bridge/references/flows.md +13 -3
- package/skills/agent-bridge/references/network.md +19 -1
- package/skills/agent-bridge/references/realtime.md +1 -1
- package/skills/agent-bridge/references/restore.md +21 -3
- package/skills/agent-bridge/references/screen.md +8 -2
- package/skills/agent-bridge/references/state.md +21 -1
package/dist/runtime/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as Transport, A as AgentBridgeOptions } from '../types-
|
|
2
|
-
export { C as CallMessage, D as DeviceInfo, L as LogEntry, O as OptionsSchema, R as RUNTIME_MARKER, b as ResultMessage, S as Scenario, c as ScenarioContext, d as Scenarios, e as ToolDefinition, f as ToolFn, g as ToolInfo, T as Tools } from '../types-
|
|
1
|
+
import { a as Transport, A as AgentBridgeOptions } from '../types-qlwdcGjt.cjs';
|
|
2
|
+
export { C as CallMessage, D as DeviceInfo, L as LogEntry, O as OptionsSchema, R as RUNTIME_MARKER, b as ResultMessage, S as Scenario, c as ScenarioContext, d as Scenarios, e as ToolDefinition, f as ToolFn, g as ToolInfo, T as Tools } from '../types-qlwdcGjt.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Works in any React Native app served by Metro. The client evaluates one fixed
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps secrets out of what store.* prints. Either dotted paths per store
|
|
3
|
+
* (`{ auth: ['accessToken', 'user.phone'] }`), or a hook called for the store
|
|
4
|
+
* value and every nested value with its dotted path; what it returns is
|
|
5
|
+
* printed instead.
|
|
6
|
+
*
|
|
7
|
+
* Paths are store paths. A `store.call` result that is a piece of the store's
|
|
8
|
+
* state (same object, e.g. `() => get().user`) is matched by where it lives; a
|
|
9
|
+
* copy or derived value (`() => ({ ...get().user })`) is not, so its paths are
|
|
10
|
+
* matched relative to the result and store paths like `user.phone` won't apply.
|
|
11
|
+
* Use the hook, or don't return secrets from actions.
|
|
12
|
+
*/
|
|
13
|
+
type Redact = Record<string, string[]> | ((store: string, path: string, value: unknown) => unknown);
|
|
14
|
+
/**
|
|
15
|
+
* Keeps secrets out of what an adapter prints, by the name of the slice or
|
|
16
|
+
* atom: dotted paths inside it (`{ auth: ['token', 'user.phone'] }`), or `true`
|
|
17
|
+
* for all of it (`{ authToken: true }`). Or a hook called for the whole value
|
|
18
|
+
* (path `''`) and every nested value with its dotted path; what it returns is
|
|
19
|
+
* printed instead.
|
|
20
|
+
*/
|
|
21
|
+
type NamedRedact = Record<string, string[] | true> | ((name: string, path: string, value: unknown) => unknown);
|
|
22
|
+
|
|
23
|
+
export type { NamedRedact as N, Redact as R };
|
|
@@ -6,6 +6,10 @@ type CallMessage = {
|
|
|
6
6
|
args?: unknown[];
|
|
7
7
|
/** Device to run on. Expo's socket broadcasts, so every app sees every call. */
|
|
8
8
|
to?: string;
|
|
9
|
+
/** A session's own: the journal tag it holds, so the reply carries the journal only when it is stale. */
|
|
10
|
+
journalAck?: string;
|
|
11
|
+
/** A session's own secret; the runtime keeps the first one it gets and ignores the journal calls of anyone else. */
|
|
12
|
+
journalKey?: string;
|
|
9
13
|
};
|
|
10
14
|
/** An error or warning the app logged, threw or left unhandled. */
|
|
11
15
|
type LogEntry = {
|
|
@@ -39,6 +43,10 @@ type ResultMessage = ({
|
|
|
39
43
|
loadId?: string;
|
|
40
44
|
/** Areas (`store`, `query`, ...) whose `*.restore` has something to undo. */
|
|
41
45
|
pending?: string[];
|
|
46
|
+
/** Goes up when something persistent changed that a restore could undo. */
|
|
47
|
+
journal?: number;
|
|
48
|
+
/** What each area could undo from storage; sent only to a session that proved it holds the key and a stale tag. */
|
|
49
|
+
journalData?: Record<string, unknown>;
|
|
42
50
|
};
|
|
43
51
|
type ToolInfo = {
|
|
44
52
|
name: string;
|
|
@@ -82,6 +90,20 @@ type ToolDefinition = ToolFn | {
|
|
|
82
90
|
pending?: (options?: {
|
|
83
91
|
detail?: boolean;
|
|
84
92
|
}) => unknown;
|
|
93
|
+
/**
|
|
94
|
+
* On a `*.restore` tool: what it could undo that lives in persistent
|
|
95
|
+
* storage (so it outlives a reload), as JSON, or undefined. A session
|
|
96
|
+
* keeps it, and hands it to `adopt` after the app reloads.
|
|
97
|
+
*/
|
|
98
|
+
journal?: () => unknown;
|
|
99
|
+
/**
|
|
100
|
+
* Takes what `journal` returned before a reload and re-arms the undo for
|
|
101
|
+
* it in the new load, without changing the app. Returns an
|
|
102
|
+
* `AdoptResult`.
|
|
103
|
+
*/
|
|
104
|
+
adopt?: (data: unknown) => unknown;
|
|
105
|
+
/** Goes up when `journal` may have changed. Not shared state: keep it in the adapter's own module. */
|
|
106
|
+
journalRev?: () => number;
|
|
85
107
|
};
|
|
86
108
|
/** Tools by name. Namespace them with a dot, e.g. `query.pin`. */
|
|
87
109
|
type Tools = Record<string, ToolDefinition>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/async-storage.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/async-storage.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/device.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/device.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/jotai.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/jotai.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/react-navigation.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/react-navigation.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/redux.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/redux.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/secure-store.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/secure-store.cjs')
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avasapp/agent-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Let coding agents drive a running React Native app directly: seed state, flip flags, navigate and check the screen in about a millisecond per call, with no taps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,14 @@
|
|
|
38
38
|
"types": "./dist/adapters/zustand.d.cts",
|
|
39
39
|
"default": "./entries/zustand.cjs"
|
|
40
40
|
},
|
|
41
|
+
"./redux": {
|
|
42
|
+
"types": "./dist/adapters/redux.d.cts",
|
|
43
|
+
"default": "./entries/redux.cjs"
|
|
44
|
+
},
|
|
45
|
+
"./jotai": {
|
|
46
|
+
"types": "./dist/adapters/jotai.d.cts",
|
|
47
|
+
"default": "./entries/jotai.cjs"
|
|
48
|
+
},
|
|
41
49
|
"./react-native-mmkv": {
|
|
42
50
|
"types": "./dist/adapters/react-native-mmkv.d.cts",
|
|
43
51
|
"default": "./entries/react-native-mmkv.cjs"
|
|
@@ -46,6 +54,10 @@
|
|
|
46
54
|
"types": "./dist/adapters/expo-router.d.cts",
|
|
47
55
|
"default": "./entries/expo-router.cjs"
|
|
48
56
|
},
|
|
57
|
+
"./react-navigation": {
|
|
58
|
+
"types": "./dist/adapters/react-navigation.d.cts",
|
|
59
|
+
"default": "./entries/react-navigation.cjs"
|
|
60
|
+
},
|
|
49
61
|
"./react-native-webview": {
|
|
50
62
|
"types": "./dist/adapters/react-native-webview.d.cts",
|
|
51
63
|
"default": "./entries/react-native-webview.cjs"
|
|
@@ -66,6 +78,18 @@
|
|
|
66
78
|
"types": "./dist/adapters/socket-io.d.cts",
|
|
67
79
|
"default": "./entries/socket.io.cjs"
|
|
68
80
|
},
|
|
81
|
+
"./async-storage": {
|
|
82
|
+
"types": "./dist/adapters/async-storage.d.cts",
|
|
83
|
+
"default": "./entries/async-storage.cjs"
|
|
84
|
+
},
|
|
85
|
+
"./secure-store": {
|
|
86
|
+
"types": "./dist/adapters/secure-store.d.cts",
|
|
87
|
+
"default": "./entries/secure-store.cjs"
|
|
88
|
+
},
|
|
89
|
+
"./device": {
|
|
90
|
+
"types": "./dist/adapters/device.d.cts",
|
|
91
|
+
"default": "./entries/device.cjs"
|
|
92
|
+
},
|
|
69
93
|
"./client": {
|
|
70
94
|
"types": "./dist/client/index.d.ts",
|
|
71
95
|
"import": "./dist/client/index.js",
|
|
@@ -92,7 +116,11 @@
|
|
|
92
116
|
"cdp",
|
|
93
117
|
"tanstack-query",
|
|
94
118
|
"zustand",
|
|
119
|
+
"redux",
|
|
120
|
+
"jotai",
|
|
95
121
|
"mmkv",
|
|
122
|
+
"async-storage",
|
|
123
|
+
"secure-store",
|
|
96
124
|
"realtime",
|
|
97
125
|
"ably",
|
|
98
126
|
"socket.io",
|
|
@@ -135,12 +163,15 @@
|
|
|
135
163
|
"devDependencies": {
|
|
136
164
|
"@changesets/cli": "^3.0.3",
|
|
137
165
|
"@expo/devtools": "^57.0.1",
|
|
166
|
+
"@reduxjs/toolkit": "^2.14.0",
|
|
138
167
|
"@tanstack/query-core": "^5.90.0",
|
|
139
168
|
"@types/bun": "^1.4.2",
|
|
140
169
|
"@types/react": "^19.3.0",
|
|
141
170
|
"@types/ws": "^8.18.1",
|
|
142
171
|
"ably": "^2.29.0",
|
|
143
172
|
"happy-dom": "^20.14.5",
|
|
173
|
+
"jotai": "^3.0.2",
|
|
174
|
+
"redux-persist": "^6.0.0",
|
|
144
175
|
"socket.io-client": "^4.8.4",
|
|
145
176
|
"tsup": "^8.5.1",
|
|
146
177
|
"typescript": "~6.0.3",
|
|
@@ -33,6 +33,11 @@ config on every call, so a project `.npmrc` with keys npm doesn't know prints
|
|
|
33
33
|
`npm warn Unknown project config` into your output each time, and it adds
|
|
34
34
|
roughly 200 ms more startup per call (machine-dependent).
|
|
35
35
|
|
|
36
|
+
If the app's tools are already in your tool list as an MCP server
|
|
37
|
+
(`agent-bridge mcp`; names like `screen_snapshot` for `screen.snapshot`), call
|
|
38
|
+
those: each takes `args`, an array of the tool's arguments, and `full: true` for
|
|
39
|
+
a result over 32 KB. If the list is empty or a call fails, call `agent_bridge_status`. A failed call keeps the session's own error (`may or may not have run`): check state before repeating a call that changes it. The rest of this skill applies the same way.
|
|
40
|
+
|
|
36
41
|
## Start a session
|
|
37
42
|
|
|
38
43
|
```sh
|
|
@@ -45,15 +50,25 @@ If Metro runs on another port, pass the same `--metro` (or set
|
|
|
45
50
|
`Nothing is listening on …` error names the Metro it found instead and its apps;
|
|
46
51
|
check it's yours before switching to it.
|
|
47
52
|
|
|
53
|
+
Calls take `--timeout <ms>` (default 10000) for a slow call. To drive two apps
|
|
54
|
+
at once, start named sessions (`session start --name ios`, `--name android`) and
|
|
55
|
+
pass `--session <name>` on each call. A session is found from the nearest
|
|
56
|
+
`package.json` above where you run: a command at a monorepo root also finds the
|
|
57
|
+
one session started in an app folder under it (downward only; a nested checkout
|
|
58
|
+
with its own `.git` doesn't count). With several app sessions under the root it
|
|
59
|
+
connects directly, so pass `--session`.
|
|
60
|
+
|
|
48
61
|
A session holds one connection for all your calls. It stops itself after 15
|
|
49
62
|
minutes without calls and runs `bridge.restore` when it does.
|
|
50
63
|
|
|
51
|
-
A JS reload (an error screen's Retry, or `app.reload`) throws away
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
A JS reload (an error screen's Retry, or `app.reload`) throws away the app's
|
|
65
|
+
undo state, except what lives in storage: string `mmkv.set`/`mmkv.delete` and
|
|
66
|
+
persisted zustand stores are kept by the session and still undone by
|
|
67
|
+
`bridge.restore`. The session reports what it kept, and
|
|
68
|
+
`app reloaded; N pending restores lost: query, ...` (the areas that had
|
|
69
|
+
something to undo) for the rest, as a warning on the next call (also on a
|
|
70
|
+
failed one) and again in `session stop`. For the lost ones, redo your setup:
|
|
71
|
+
they will not be undone.
|
|
57
72
|
|
|
58
73
|
If a call fails with `app not connected; reconnecting` or `The app is gone`, the
|
|
59
74
|
session lost the app (Metro or the app is down, or mid-reload). It retries on
|
|
@@ -61,6 +76,13 @@ its 5 s health check and on your next call: wait a few seconds and call again.
|
|
|
61
76
|
`session stop` still works. A session never moves to another app: `<app> is
|
|
62
77
|
not connected; connected: …` means its app is gone and something else is there.
|
|
63
78
|
|
|
79
|
+
If a call fails with `the app reloaded during <tool>; it may or may not have run`
|
|
80
|
+
(or `disconnected`, `the connection dropped`), the connection dropped mid-call
|
|
81
|
+
and the session did not repeat it (only read-only tools are repeated). Check the screen or state before you call again.
|
|
82
|
+
|
|
83
|
+
A `timeoutMs` in the options of `screen.waitFor` or `webview.waitFor` longer
|
|
84
|
+
than the call timeout stretches it, so you don't need `--timeout` for that.
|
|
85
|
+
|
|
64
86
|
If `tools` fails with "agent-bridge isn't running", the hook isn't mounted in
|
|
65
87
|
this build. Say so; don't fall back to tapping silently.
|
|
66
88
|
|
|
@@ -99,15 +121,23 @@ real account instead. If you're asked to add one, read
|
|
|
99
121
|
## See and use the screen
|
|
100
122
|
|
|
101
123
|
```sh
|
|
102
|
-
npx @avasapp/agent-bridge call screen.snapshot #
|
|
124
|
+
npx @avasapp/agent-bridge call screen.snapshot # one line per button, input, text on the focused screen (--json for rects)
|
|
103
125
|
npx @avasapp/agent-bridge call screen.press '"add-plant"' # by testID, label or text
|
|
104
126
|
npx @avasapp/agent-bridge call screen.fill '["plant-name", "Fern"]' # runs the input's handlers
|
|
105
127
|
npx @avasapp/agent-bridge call screen.waitFor '"Name is required"' # {"gone": true} waits for it to go
|
|
106
128
|
npx @avasapp/agent-bridge call router.navigate /inbox
|
|
107
129
|
```
|
|
108
130
|
|
|
131
|
+
Check `tools` for `router.*` (expo-router) or `nav.*` (React Navigation). `nav.*`
|
|
132
|
+
takes route names (`nav.navigate Inbox`, `nav.popTo Home`), and paths only if the
|
|
133
|
+
app passed `linking`; read the returned `warning`.
|
|
134
|
+
|
|
109
135
|
`press` and `fill` return once the app has rendered the result, so the next
|
|
110
|
-
check sees it.
|
|
136
|
+
check sees it. `press` on plain text presses the nearest pressable ancestor.
|
|
137
|
+
Unfocused tabs (native tabs included) and screens under others count as hidden.
|
|
138
|
+
Screen tools need React Native's New Architecture (Fabric); where nothing can
|
|
139
|
+
be measured they fail with a message saying so (`snapshot --all` and
|
|
140
|
+
`findText` still return text, without rects). Arguments are a JSON array (or a single JSON value): the array is
|
|
111
141
|
spread into the tool's arguments.
|
|
112
142
|
|
|
113
143
|
## Watch for errors
|
|
@@ -137,6 +167,7 @@ from the real state.
|
|
|
137
167
|
| Read or set query data, store values, routes, modals; get argument shapes right | [references/state.md](references/state.md) |
|
|
138
168
|
| Mock or inspect requests, or deal with strict network mode | [references/network.md](references/network.md) |
|
|
139
169
|
| See or fake realtime messages (`realtime.*` in `tools`) | [references/realtime.md](references/realtime.md) |
|
|
170
|
+
| Fake a deep link, background, offline, a push or the clock (`device.*` in `tools`) | [references/device.md](references/device.md) |
|
|
140
171
|
| Handle a result over 32 KB or an argument over ~128 KB | [references/large-data.md](references/large-data.md) |
|
|
141
172
|
| Run many calls at once, write a flow file, or measure timing | [references/flows.md](references/flows.md) |
|
|
142
173
|
| Understand what restore undoes, or fix a value restore got wrong | [references/restore.md](references/restore.md) |
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Device events (`device.*`)
|
|
2
|
+
|
|
3
|
+
Only there if the app added `deviceTools`; check `bridge.tools`.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx @avasapp/agent-bridge call device.openUrl '"myapp://orders/5"' # deep link into the running app; returns the route when the app passes navigation
|
|
7
|
+
npx @avasapp/agent-bridge call device.appState '"background"' # then '"active"'
|
|
8
|
+
npx @avasapp/agent-bridge call device.offline true # fetch, XHR and NetInfo; false goes back
|
|
9
|
+
npx @avasapp/agent-bridge call device.notification '[{"title":"Hi","data":{"id":5}},{"tapped":true}]'
|
|
10
|
+
npx @avasapp/agent-bridge call device.setTime '"2030-01-02T03:04:05Z"' # only with time: true
|
|
11
|
+
npx @avasapp/agent-bridge call device.restore
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- `device.openUrl` is for a running app. A cold start reads its URL before the bridge can fake it: open that link with agent-device.
|
|
15
|
+
- `device.offline` fails mocked requests too. Metro stays reachable.
|
|
16
|
+
- Without `{tapped:true}` a notification calls the received listeners (app open); with it, the response listeners.
|
|
17
|
+
- The clock moves `Date.now()` and `new Date()` only; timers do not move. After `device.setTime`, call `device.appState '"background"'` then `'"active"'` so query caches recheck staleness (`active` alone does nothing when the app is already active). JS-driven `Animated` animations and Reanimated worklets are not shifted safely.
|
|
18
|
+
- `device.offline` shows in `net.log` as `(device offline)`. NetInfo is faked only for listeners, `useNetInfo` and `NetInfo.refresh`; `fetch('wifi')` and a real native network event are not.
|
|
19
|
+
- `device.notification` returns `listeners`; 0 means nothing heard it.
|
|
20
|
+
- `device.openUrl` also reaches `useLinkingURL()`, but `getLinkingURL()` still reads the native value.
|
|
21
|
+
- `bridge.pending` lists what is set under `device`, and `bridge.restore` undoes it.
|
|
@@ -35,11 +35,10 @@ result, and for timing checks. Name it `flows/<name>.flow.mjs` in the app: plain
|
|
|
35
35
|
```js
|
|
36
36
|
export const scenario = 'signedIn' // applied first, undone after, even on failure
|
|
37
37
|
|
|
38
|
-
export default async ({ step }) => {
|
|
38
|
+
export default async ({ step, expect }) => {
|
|
39
39
|
await step('open form', 'screen.press', 'add-plant')
|
|
40
40
|
await step('save empty', 'screen.press', 'save-plant')
|
|
41
|
-
await
|
|
42
|
-
await step('undo', 'bridge.restore')
|
|
41
|
+
await expect.text('Name is required') // also expect.gone, expect.route, expect.result
|
|
43
42
|
}
|
|
44
43
|
```
|
|
45
44
|
|
|
@@ -56,6 +55,17 @@ when the flow fails. The flow gets what each scenario's `apply` returned as
|
|
|
56
55
|
`scenarios.signedIn`. A failed restorer makes the run exit non-zero.
|
|
57
56
|
`--scenario` replaces a declared one of the same name.
|
|
58
57
|
|
|
58
|
+
Run several at once: `run flows/` (a directory's `*.flow.mjs`), several files, or a
|
|
59
|
+
glob, in order on one connection, with a pass/fail line each and a summary; exit 1 if
|
|
60
|
+
any failed. `--bail` stops at the first failure, `--grep <text>` filters by name or path
|
|
61
|
+
(relative to here), `--reporter json|junit` is for CI: report on stdout with all
|
|
62
|
+
other output on stderr, or `--out report.xml`. Globs only match `*.flow.mjs`. Every flow ends with
|
|
63
|
+
`bridge.restore`, pass or fail (it is global: through a session it also undoes what
|
|
64
|
+
you set up by hand first); `--keep` skips it to debug. A failed flow prints the
|
|
65
|
+
screen elements and route. `expect.text(t)`, `expect.gone(t)`, `expect.route(path)`
|
|
66
|
+
and `expect.result(tool, args, match)` (a RegExp matches a string) throw readable
|
|
67
|
+
messages.
|
|
68
|
+
|
|
59
69
|
`step` takes the same argument shapes as `call`; see [state.md](state.md).
|
|
60
70
|
|
|
61
71
|
## Timing
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
```sh
|
|
4
4
|
npx @avasapp/agent-bridge call net.mock '["/inbox", {"status": 500}]' # or {"offline": true}
|
|
5
5
|
npx @avasapp/agent-bridge call net.log '[{"since": 1700000000000}]' # entries carry startedAt (epoch ms)
|
|
6
|
-
npx @avasapp/agent-bridge call net.entry 3 # one request
|
|
6
|
+
npx @avasapp/agent-bridge call net.entry 3 # one request: bodies whole, headers (credential headers such as Authorization, Cookie, *-token, *-api-key cut unless {"full": true} second)
|
|
7
|
+
npx @avasapp/agent-bridge call net.waitFor '[{"path": "/orders", "method": "POST"}, {"timeoutMs": 5000}]' # wait for it to finish
|
|
7
8
|
npx @avasapp/agent-bridge call net.mockFromLog '[3, {"user": {"name": "Moss"}}]' # a real response, patched
|
|
8
9
|
npx @avasapp/agent-bridge call net.mocks '[{"full": true}]' # active mocks; bodies over ~2 KB are cut unless full
|
|
9
10
|
```
|
|
@@ -15,6 +16,23 @@ The newest mock is tried first, so a later broad mock shadows an earlier
|
|
|
15
16
|
specific one: give the specific one `{"priority": 1}` as `net.mock`'s third
|
|
16
17
|
argument, or read `shadows` in its result.
|
|
17
18
|
|
|
19
|
+
## What a mock matches
|
|
20
|
+
|
|
21
|
+
A string is a URL substring: `"/orders"` also answers `/orders/5/items`. For
|
|
22
|
+
exact matches use the object form (every key given must match):
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
call net.mock '[{"path": "/orders/:id"}, {"json": {"id": 5}}]' # whole path, any host, query ignored; * is the rest
|
|
26
|
+
call net.mock '[{"graphql": "GetUser"}, {"json": {"data": {}}}]' # operationName in the POST body or ?operationName=
|
|
27
|
+
call net.mock '[{"path": "/search", "body": {"filter": {"status": "open"}}}, {"json": []}]' # JSON body contains this
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`path` ignores case and the query, takes `:name` only as a whole segment (`/items:batchGet` is literal), and a trailing slash is optional; spaces are refused (write `%20`). A mock with only `body` or `graphql` looks at every request. `body` matches objects by key (extra keys are fine) and arrays by length and
|
|
31
|
+
position; form and binary bodies never match. A batched GraphQL array body
|
|
32
|
+
isn't matched by `graphql`; use `body`. `net.mockFromLog` returns a `warning` when a POST body wasn't kept and the mock may answer other operations. The reply's `matches` says it in words.
|
|
33
|
+
`net.waitFor` takes the same match; pass `{"sinceId": <id from net.log>}` so an
|
|
34
|
+
older request doesn't satisfy it.
|
|
35
|
+
|
|
18
36
|
For a mock body too big for the command line, pass it as `@file`; see
|
|
19
37
|
[large-data.md](large-data.md).
|
|
20
38
|
|
|
@@ -5,7 +5,7 @@ them instead of waiting for a backend to send one:
|
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
7
|
npx @avasapp/agent-bridge call realtime.channels # what the app listens on
|
|
8
|
-
npx @avasapp/agent-bridge call realtime.log '{"channel": "chat"}' # recent messages
|
|
8
|
+
npx @avasapp/agent-bridge call realtime.log '{"channel": "chat"}' # recent messages: real, faked, and `sent: true` ones the app sent (credential keys are [redacted] unless `{"full":true}`)
|
|
9
9
|
npx @avasapp/agent-bridge call realtime.mute '"chat"' # stop real ones overwriting your state
|
|
10
10
|
npx @avasapp/agent-bridge call realtime.emit '["chat", {"name": "message", "data": {"text": "hi"}}]'
|
|
11
11
|
npx @avasapp/agent-bridge call realtime.connection '"disconnected"' # null goes back to the real state
|
|
@@ -7,14 +7,21 @@ npx @avasapp/agent-bridge session stop # runs bridge.restore
|
|
|
7
7
|
npx @avasapp/agent-bridge session stop --keep # leaves the app as is; warns about anything still pending
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
`bridge.restore` unpins queries, puts back store and
|
|
10
|
+
`bridge.restore` unpins queries, puts back store, MMKV, AsyncStorage and SecureStore values, removes
|
|
11
11
|
network mocks, unmutes realtime channels and ends a faked connection state,
|
|
12
12
|
then undoes active scenarios (newest first), so the next run starts from the
|
|
13
13
|
real state. With `session stop --keep`, a later restore will still apply what
|
|
14
14
|
is pending.
|
|
15
15
|
|
|
16
|
-
A JS reload throws away
|
|
17
|
-
|
|
16
|
+
A JS reload throws away the app's undo state. In a session, what lives in
|
|
17
|
+
storage is kept: `mmkv.set`/`mmkv.delete` of string values and `store.set`/
|
|
18
|
+
`store.call` on a persisted zustand store (one made with `persist`) are still
|
|
19
|
+
undone by `bridge.restore` after the reload, and the next call says
|
|
20
|
+
`app reloaded; kept N undos across the reload (...)`. A value the app changed
|
|
21
|
+
after your write is left alone and named. Everything else (stores that are not
|
|
22
|
+
persisted, redux, jotai, AsyncStorage and SecureStore changes, queries, mocks, `onRestore`) is lost: the session warns
|
|
23
|
+
`app reloaded; N pending restores lost: ...`. Redo that setup when you see it. After a reload that also lost non-persisted changes, check `bridge.pending`: `kept N undos` does not list what was lost in the same area. `not kept across reload: store:key` means that key (a Set, Date, class instance, one `partialize` renames, or one `redact` hides) is not journaled, so the reload keeps the agent's value for it; `could not re-arm after the reload: ...` means a store or MMKV instance never became ready and its undo was dropped; `<tool> was cut off; ... that undo may be lost` means a write's reply was lost and may not be journaled.
|
|
24
|
+
AsyncStorage and SecureStore values stay on disk across a reload with their old values gone, so run `bridge.restore` before `app.reload`. A key the app wrote after the agent did is left alone and returned under `skipped`.
|
|
18
25
|
|
|
19
26
|
## Mocked queries
|
|
20
27
|
|
|
@@ -38,6 +45,17 @@ with `store.set` snapshots the broken one.
|
|
|
38
45
|
keeps the current value and drops the snapshot.
|
|
39
46
|
- `store.set <store> {}` arms the snapshot before you drive the UI.
|
|
40
47
|
|
|
48
|
+
`redux.*` and `jotai.*` work the same way, under their own names. `redux.set`
|
|
49
|
+
snapshots each slice on its first change and restore puts back only those
|
|
50
|
+
slices; `redux.dispatch` snapshots the whole state on its first call, so restore
|
|
51
|
+
puts all of it back, including what the app changed in other slices after that.
|
|
52
|
+
A slice or atom the app changed after the agent is left alone and returned in
|
|
53
|
+
`{restored, skipped}`. `redux.commit` drops the snapshots. Jotai snapshots each
|
|
54
|
+
atom on its first `jotai.set`; `jotai.commit <atom>` drops one. These snapshots
|
|
55
|
+
live in memory, so **restore before `app.reload`**: with redux-persist or
|
|
56
|
+
`atomWithStorage` the agent's values stay saved until then. With an async
|
|
57
|
+
`atomWithStorage`, `jotai.get` the atom before the first set.
|
|
58
|
+
|
|
41
59
|
## WebViews
|
|
42
60
|
|
|
43
61
|
`webview.press`, `fill` and `send` change the page and app, and restore does
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
# See and use the screen
|
|
2
2
|
|
|
3
3
|
```sh
|
|
4
|
-
npx @avasapp/agent-bridge call screen.snapshot #
|
|
4
|
+
npx @avasapp/agent-bridge call screen.snapshot # one line per element: `button #save "Save" disabled` (`hidden` counts what was skipped; {"all":true} lists everything)
|
|
5
|
+
npx @avasapp/agent-bridge call screen.snapshot '{"kinds":["input","button"],"within":"checkout-form"}' # only those kinds, only inside that target
|
|
6
|
+
npx @avasapp/agent-bridge call screen.snapshot --json # JSON with rects and `role` (the text form already shows actions=, readonly, offscreen); `@x,y` on a line is `{"at":[x,y]}`, `index=N` is `{"index":N}`
|
|
5
7
|
npx @avasapp/agent-bridge call screen.findText "Payment" # text or accessibility label; {"labels":false} for text only
|
|
6
8
|
npx @avasapp/agent-bridge call screen.press '"add-plant"' # by testID, label or text
|
|
7
9
|
npx @avasapp/agent-bridge call screen.press '[{"at":[350,60]}]' # icon-only button: by point; {"index":1} picks among matches; {"force":true} presses disabled
|
|
8
10
|
npx @avasapp/agent-bridge call screen.press '["save-plant", {"scroll":true}]' # scrolls an off-screen target into view first
|
|
9
11
|
npx @avasapp/agent-bridge call screen.press '"Swipe to confirm"' # no onPress: sends the "activate" accessibility action
|
|
10
12
|
npx @avasapp/agent-bridge call screen.press '["Volume", {"action":"increment"}]' # any action in the element's snapshot `actions`
|
|
13
|
+
npx @avasapp/agent-bridge call screen.press '["Item 80", {"scroll":true}]' # a row a FlatList has not rendered yet: pages the main list until it shows up (waitFor does not page)
|
|
14
|
+
npx @avasapp/agent-bridge call screen.press '["Message", {"long":true}]' # onPressIn, onLongPress, onPressOut; errors when there is no onLongPress
|
|
15
|
+
npx @avasapp/agent-bridge call screen.press '"dark-mode"' # a Switch is toggled with onValueChange(!value)
|
|
16
|
+
npx @avasapp/agent-bridge call screen.setValue '["volume", 7]' # Switch true/false, Slider number, Picker or SegmentedControl item; reads the value back
|
|
11
17
|
npx @avasapp/agent-bridge call screen.scroll '"Notes"' # bring a target to the middle of its ScrollView/FlatList; {"toEnd":true}, {"by":400}, [{"toEnd":true},{"within":"list"}]
|
|
12
18
|
npx @avasapp/agent-bridge call screen.refresh # pull to refresh (the RefreshControl's onRefresh); then screen.waitFor the data
|
|
13
19
|
npx @avasapp/agent-bridge call screen.fill '["plant-name", "Fern"]' # runs the input's handlers
|
|
@@ -16,7 +22,7 @@ npx @avasapp/agent-bridge call screen.waitFor '["Section title", {"scroll":true}
|
|
|
16
22
|
```
|
|
17
23
|
|
|
18
24
|
`press` and `fill` return once the app has rendered the result, so the next
|
|
19
|
-
check sees it. `fill` skips the keyboard: autocorrect and native-only input
|
|
25
|
+
check sees it. `fill` also takes `{"submit":true}` and `{"type":true}` (per-character, for debounced search); `""` clears. A press that hit an ancestor's handler says `via: "ancestor"`: check it did what you meant. `fill` skips the keyboard: autocorrect and native-only input
|
|
20
26
|
behaviour need one real typing step from agent-device.
|
|
21
27
|
|
|
22
28
|
A swipe-to-confirm or slider with no `onPress` shows as a button (or a view)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
npx @avasapp/agent-bridge call query.pin '[["features"], <data>]' # stays through refetches
|
|
7
7
|
npx @avasapp/agent-bridge call store.call '["settings", "setTheme", "dark"]'
|
|
8
8
|
npx @avasapp/agent-bridge call router.navigate /inbox
|
|
9
|
-
npx @avasapp/agent-bridge call router.current
|
|
9
|
+
npx @avasapp/agent-bridge call router.current # React Navigation apps: nav.navigate Inbox, nav.current
|
|
10
10
|
npx @avasapp/agent-bridge call router.dismiss # closes a route-based modal
|
|
11
11
|
```
|
|
12
12
|
|
|
@@ -31,6 +31,26 @@ npx @avasapp/agent-bridge call store.get '["app", {"keys": true}]' # top-level
|
|
|
31
31
|
`null` reply means the data is undefined. For a large query, narrow it with
|
|
32
32
|
`pages` or `path`; see [large-data.md](large-data.md).
|
|
33
33
|
|
|
34
|
+
## Redux and Jotai
|
|
35
|
+
|
|
36
|
+
Same ideas under their own prefixes, so a Zustand `store.*` and these can coexist.
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npx @avasapp/agent-bridge call redux.list
|
|
40
|
+
npx @avasapp/agent-bridge call redux.get '["auth", "user.name"]' # or [] for the whole state
|
|
41
|
+
npx @avasapp/agent-bridge call redux.dispatch '[{"type":"cart/add","payload":"apple"}]'
|
|
42
|
+
npx @avasapp/agent-bridge call redux.set '["auth", {"token": "abc"}]' # merge into a slice
|
|
43
|
+
npx @avasapp/agent-bridge call jotai.list # read-write or read-only
|
|
44
|
+
npx @avasapp/agent-bridge call jotai.get '["user", "name"]'
|
|
45
|
+
npx @avasapp/agent-bridge call jotai.set '["count", 5]' # writable atoms only
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`redux.set` and restore need the app to wrap its root reducer in
|
|
49
|
+
`bridgeReducer()` (inside `persistReducer` with redux-persist); without it they
|
|
50
|
+
fail and say so. `redux.set` merges into object slices only. `redux.dispatch`
|
|
51
|
+
sends plain actions only. `jotai.set` refuses atoms whose value isn't the one
|
|
52
|
+
sent (action atoms, `atomWithReducer`). Redacted slices, atoms and paths print as `"[redacted]"`.
|
|
53
|
+
|
|
34
54
|
## Argument shapes
|
|
35
55
|
|
|
36
56
|
Arguments are a JSON array (or a single JSON value): the array is spread into
|