@deeeed/metamask-harness 0.41.1 → 0.43.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/CHANGELOG.md +35 -0
- package/README.md +12 -0
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +108 -10
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +117 -16
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +2 -0
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +921 -0
- package/adapters/mobile/bridge-runtime/lib/config.cjs +14 -4
- package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +177 -0
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +13 -3
- package/adapters/mobile/reload-app.mjs +67 -0
- package/adapters/mobile/start-console-forwarder.sh +22 -3
- package/adapters/mobile/start-metro.sh +6 -11
- package/adapters/mobile/stop-metro.sh +10 -1
- package/adapters/shared/open-debug.mjs +172 -2
- package/dist/adapters/extension/browser-cdp.js +174 -0
- package/dist/adapters/extension/network-observer.js +209 -0
- package/dist/adapters/extension/performance-observer.js +75 -0
- package/dist/adapters/mobile/frame-metrics.js +45 -0
- package/dist/adapters/mobile/metro-env.js +0 -5
- package/dist/adapters/mobile/performance-observer.js +43 -0
- package/dist/adapters/mobile/prepare.js +1 -3
- package/dist/adapters/mobile/runtime-decision.js +6 -9
- package/dist/adapters/performance/cdp-trace.js +342 -0
- package/dist/adapters/performance/js-task-metrics.js +35 -0
- package/dist/adapters.js +29 -1
- package/dist/artifact-files.js +92 -0
- package/dist/async.js +19 -0
- package/dist/cli-commands.js +6 -3
- package/dist/cli.js +4 -0
- package/dist/command-contract.js +3 -0
- package/dist/commands/call.js +66 -20
- package/dist/commands/reload.js +80 -0
- package/dist/commands/run-engine.js +18 -0
- package/dist/commands/run.js +68 -22
- package/dist/mm-harness-cli.js +17 -1
- package/dist/network-observation.js +283 -0
- package/dist/performance-observation.js +465 -0
- package/docs/NETWORK-CAPTURE.md +98 -0
- package/docs/PERFORMANCE-CAPTURE.md +33 -0
- package/docs/RECIPES.md +17 -0
- package/library/actions/mobile/app/network_assert.mjs +14 -0
- package/library/actions/mobile/app/network_capture.mjs +72 -0
- package/library/actions/mobile/platform/bridge.mjs +10 -2
- package/library/actions/shared/app/network-artifact.mjs +10 -0
- package/library/actions/shared/app/network-assert.mjs +154 -0
- package/library/manifests/extension.action-manifest.json +173 -0
- package/library/manifests/mobile.action-manifest.json +204 -0
- package/library/recipes/mobile/perps/performance.recipe.json +11 -11
- package/package.json +1 -1
- package/scripts/completions.sh +2 -1
- package/scripts/site-contrast.mjs +43 -27
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
bridgeCommand,
|
|
8
|
+
runAdapter,
|
|
9
|
+
} from '../platform/bridge.mjs';
|
|
10
|
+
import { resolveNetworkArtifact } from '../../shared/app/network-artifact.mjs';
|
|
11
|
+
|
|
12
|
+
export async function captureNetwork(input) {
|
|
13
|
+
const node = input.node ?? {};
|
|
14
|
+
const phase = String(node.phase ?? '').toLowerCase();
|
|
15
|
+
const id = String(node.id ?? '').trim();
|
|
16
|
+
if (!['start', 'end'].includes(phase) || !id) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'app.network_capture requires phase=start|end and a non-empty id.',
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (phase === 'start') {
|
|
23
|
+
const result = await bridgeCommand(input, [
|
|
24
|
+
'network-capture-start',
|
|
25
|
+
JSON.stringify({
|
|
26
|
+
id,
|
|
27
|
+
urlIncludes: node.url_includes ?? [],
|
|
28
|
+
methods: node.methods ?? [],
|
|
29
|
+
bodyJsonFields: node.body_json_fields ?? [],
|
|
30
|
+
maxRequests: node.max_requests,
|
|
31
|
+
maxDurationMs: node.max_duration_ms,
|
|
32
|
+
}),
|
|
33
|
+
]);
|
|
34
|
+
return { action: input.action, phase, ...result };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const artifactsDir = input.context?.artifactsDir;
|
|
38
|
+
if (!artifactsDir) {
|
|
39
|
+
throw new Error('app.network_capture requires artifactsDir.');
|
|
40
|
+
}
|
|
41
|
+
const artifactPath = String(
|
|
42
|
+
node.artifact_path ?? `network/${id}-summary.json`,
|
|
43
|
+
);
|
|
44
|
+
const artifactFile = resolveNetworkArtifact(artifactsDir, artifactPath);
|
|
45
|
+
|
|
46
|
+
const summary = await bridgeCommand(input, [
|
|
47
|
+
'network-capture-end',
|
|
48
|
+
JSON.stringify({ id }),
|
|
49
|
+
]);
|
|
50
|
+
await mkdir(path.dirname(artifactFile), { recursive: true });
|
|
51
|
+
await writeFile(artifactFile, `${JSON.stringify(summary, null, 2)}\n`);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
action: input.action,
|
|
55
|
+
phase,
|
|
56
|
+
...summary,
|
|
57
|
+
artifacts: [
|
|
58
|
+
{
|
|
59
|
+
path: artifactPath,
|
|
60
|
+
type: 'report',
|
|
61
|
+
nodeId: String(input.context?.nodeId ?? 'network-capture'),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
process.argv[1] &&
|
|
69
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
70
|
+
) {
|
|
71
|
+
runAdapter(captureNetwork);
|
|
72
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { constants as fsConstants } from 'node:fs';
|
|
2
|
+
import { constants as fsConstants, existsSync } from 'node:fs';
|
|
3
3
|
import { mkdir, mkdtemp, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { execFile, spawn } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
@@ -7,6 +7,8 @@ import os from 'node:os';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import bridgeErrors from '../../../../adapters/mobile/bridge-runtime/lib/bridge-errors.cjs';
|
|
10
|
+
import brokerModule from '../../../../adapters/mobile/bridge-runtime/lib/cdp-broker.cjs';
|
|
11
|
+
import configModule from '../../../../adapters/mobile/bridge-runtime/lib/config.cjs';
|
|
10
12
|
import { resolveMobileToolPath } from './tool-paths.mjs';
|
|
11
13
|
|
|
12
14
|
const {
|
|
@@ -16,6 +18,8 @@ const {
|
|
|
16
18
|
coded,
|
|
17
19
|
parseErrorMarker,
|
|
18
20
|
} = bridgeErrors;
|
|
21
|
+
const { brokerSocketPath } = brokerModule;
|
|
22
|
+
const { resolvePort } = configModule;
|
|
19
23
|
|
|
20
24
|
// Re-exported so the TS adapter classifies on the same code constants without a
|
|
21
25
|
// second import path into the cjs bridge-runtime.
|
|
@@ -430,7 +434,11 @@ export async function bridgeCommand(input, args) {
|
|
|
430
434
|
const script = bridgeScript(input);
|
|
431
435
|
// bridgeEnv is async: it may call `adb getprop` to resolve the Metro device name.
|
|
432
436
|
const env = await bridgeEnv(input);
|
|
433
|
-
|
|
437
|
+
const brokerSocket = brokerSocketPath(
|
|
438
|
+
path.dirname(resolveBridgeLockPath(input, env)),
|
|
439
|
+
resolvePort(env, input.context.projectRoot),
|
|
440
|
+
);
|
|
441
|
+
if (adapterActionActive && !existsSync(brokerSocket)) {
|
|
434
442
|
env.CDP_BRIDGE_LOCK_OWNER_PID = await acquireActionBridgeLock(input, env);
|
|
435
443
|
}
|
|
436
444
|
const result = await new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export function resolveNetworkArtifact(artifactsDir, relativePath) {
|
|
4
|
+
const root = path.resolve(artifactsDir);
|
|
5
|
+
const resolved = path.resolve(root, relativePath);
|
|
6
|
+
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
|
|
7
|
+
throw new Error('Network artifact_path escapes artifactsDir.');
|
|
8
|
+
}
|
|
9
|
+
return resolved;
|
|
10
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import { resolveNetworkArtifact } from './network-artifact.mjs';
|
|
5
|
+
|
|
6
|
+
export async function assertNetwork(input) {
|
|
7
|
+
const node = input.node ?? {};
|
|
8
|
+
const id = String(node.id ?? '').trim();
|
|
9
|
+
const artifactsDir = input.context?.artifactsDir;
|
|
10
|
+
if (!id || !artifactsDir) {
|
|
11
|
+
throw new Error('app.network_assert requires id and artifactsDir.');
|
|
12
|
+
}
|
|
13
|
+
const artifactPath = String(
|
|
14
|
+
node.artifact_path ?? `network/${id}-summary.json`,
|
|
15
|
+
);
|
|
16
|
+
const artifactFile = resolveNetworkArtifact(artifactsDir, artifactPath);
|
|
17
|
+
const handle = await open(
|
|
18
|
+
artifactFile,
|
|
19
|
+
constants.O_RDONLY | constants.O_NOFOLLOW,
|
|
20
|
+
);
|
|
21
|
+
let summary;
|
|
22
|
+
try {
|
|
23
|
+
const artifactStat = await handle.stat();
|
|
24
|
+
if (!artifactStat.isFile() || artifactStat.size > 5 * 1024 * 1024) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'app.network_assert summary is not a bounded regular file.',
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
summary = JSON.parse(await handle.readFile('utf8'));
|
|
30
|
+
} finally {
|
|
31
|
+
await handle.close();
|
|
32
|
+
}
|
|
33
|
+
validateSummary(summary, id);
|
|
34
|
+
|
|
35
|
+
const requiredStatus = node.required_status;
|
|
36
|
+
if (requiredStatus && summary.status !== requiredStatus) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`app.network_assert expected ${requiredStatus}, got ${summary.status}.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const requiredMinRequests = node.required_min_requests;
|
|
42
|
+
const requiredMaxRequests = node.required_max_requests;
|
|
43
|
+
const requiredTypes = node.required_types ?? [];
|
|
44
|
+
const forbiddenTypes = node.forbidden_types ?? [];
|
|
45
|
+
if (
|
|
46
|
+
(requiredTypes.length > 0 || forbiddenTypes.length > 0) &&
|
|
47
|
+
!summary.projectedBodyFields.includes('type')
|
|
48
|
+
) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
'app.network_assert type assertions require body_json_fields to include type.',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (
|
|
54
|
+
(requiredMaxRequests !== undefined || forbiddenTypes.length > 0) &&
|
|
55
|
+
requiredStatus !== 'complete'
|
|
56
|
+
) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
'app.network_assert negative assertions require required_status=complete.',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
for (const [name, value] of [
|
|
62
|
+
['required_min_requests', requiredMinRequests],
|
|
63
|
+
['required_max_requests', requiredMaxRequests],
|
|
64
|
+
]) {
|
|
65
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
|
|
66
|
+
throw new Error(`app.network_assert ${name} is invalid.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (
|
|
70
|
+
Number.isInteger(requiredMinRequests) &&
|
|
71
|
+
Number.isInteger(requiredMaxRequests) &&
|
|
72
|
+
requiredMinRequests > requiredMaxRequests
|
|
73
|
+
) {
|
|
74
|
+
throw new Error('app.network_assert request assertion range is invalid.');
|
|
75
|
+
}
|
|
76
|
+
if (
|
|
77
|
+
Number.isInteger(requiredMinRequests) &&
|
|
78
|
+
summary.totalRequests < requiredMinRequests
|
|
79
|
+
) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`app.network_assert expected at least ${requiredMinRequests} request(s), got ${summary.totalRequests}.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
Number.isInteger(requiredMaxRequests) &&
|
|
86
|
+
summary.totalRequests > requiredMaxRequests
|
|
87
|
+
) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`app.network_assert expected at most ${requiredMaxRequests} request(s), got ${summary.totalRequests}.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
for (const type of requiredTypes) {
|
|
93
|
+
if (!hasPositiveOwnCount(summary.requestsByType, type)) {
|
|
94
|
+
throw new Error(`app.network_assert did not observe required type ${type}.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const type of forbiddenTypes) {
|
|
98
|
+
if (hasPositiveOwnCount(summary.requestsByType, type)) {
|
|
99
|
+
throw new Error(`app.network_assert observed forbidden type ${type}.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
action: input.action,
|
|
105
|
+
id,
|
|
106
|
+
artifactPath,
|
|
107
|
+
status: summary.status,
|
|
108
|
+
totalRequests: summary.totalRequests,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hasPositiveOwnCount(counts, key) {
|
|
113
|
+
return (
|
|
114
|
+
Object.hasOwn(counts, key) &&
|
|
115
|
+
Number.isInteger(counts[key]) &&
|
|
116
|
+
counts[key] > 0
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validateSummary(summary, expectedId) {
|
|
121
|
+
if (
|
|
122
|
+
!summary ||
|
|
123
|
+
typeof summary !== 'object' ||
|
|
124
|
+
Array.isArray(summary) ||
|
|
125
|
+
summary.schemaVersion !== 1 ||
|
|
126
|
+
summary.id !== expectedId ||
|
|
127
|
+
!['complete', 'partial', 'unavailable'].includes(summary.status) ||
|
|
128
|
+
!Number.isInteger(summary.totalRequests) ||
|
|
129
|
+
summary.totalRequests < 0 ||
|
|
130
|
+
!Array.isArray(summary.requests) ||
|
|
131
|
+
summary.requests.length !== summary.totalRequests ||
|
|
132
|
+
!Number.isInteger(summary.uninspectableBodyRequests) ||
|
|
133
|
+
summary.uninspectableBodyRequests < 0 ||
|
|
134
|
+
!Array.isArray(summary.projectedBodyFields) ||
|
|
135
|
+
summary.projectedBodyFields.some((field) => typeof field !== 'string') ||
|
|
136
|
+
!summary.requestsByType ||
|
|
137
|
+
typeof summary.requestsByType !== 'object' ||
|
|
138
|
+
Array.isArray(summary.requestsByType)
|
|
139
|
+
) {
|
|
140
|
+
throw new Error('app.network_assert summary contract is invalid.');
|
|
141
|
+
}
|
|
142
|
+
const typeCounts = Object.values(summary.requestsByType);
|
|
143
|
+
for (const count of typeCounts) {
|
|
144
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
145
|
+
throw new Error('app.network_assert summary type counts are invalid.');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
typeCounts.reduce((total, count) => total + count, 0) !==
|
|
150
|
+
summary.totalRequests
|
|
151
|
+
) {
|
|
152
|
+
throw new Error('app.network_assert summary type counts are inconsistent.');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -1119,6 +1119,179 @@
|
|
|
1119
1119
|
}
|
|
1120
1120
|
]
|
|
1121
1121
|
},
|
|
1122
|
+
"app.network_capture": {
|
|
1123
|
+
"description": "Capture redacted HTTP requests between named Recipe Protocol v1 nodes through the adapter's persistent Network observer.",
|
|
1124
|
+
"examples": [
|
|
1125
|
+
{
|
|
1126
|
+
"action": "app.network_capture",
|
|
1127
|
+
"phase": "start",
|
|
1128
|
+
"id": "perps-home",
|
|
1129
|
+
"url_includes": ["api.hyperliquid.xyz/info"],
|
|
1130
|
+
"methods": ["POST"],
|
|
1131
|
+
"body_json_fields": ["type", "req.coin", "dex"],
|
|
1132
|
+
"intent": "Start the redacted request window",
|
|
1133
|
+
"next": "exercise-flow"
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
"action": "app.network_capture",
|
|
1137
|
+
"phase": "end",
|
|
1138
|
+
"id": "perps-home",
|
|
1139
|
+
"artifact_path": "network/perps-home.json",
|
|
1140
|
+
"intent": "End and index the redacted request window",
|
|
1141
|
+
"next": "assert-network"
|
|
1142
|
+
}
|
|
1143
|
+
],
|
|
1144
|
+
"schema": {
|
|
1145
|
+
"type": "object",
|
|
1146
|
+
"properties": {
|
|
1147
|
+
"phase": { "type": "string", "enum": ["start", "end"] },
|
|
1148
|
+
"id": { "type": "string" },
|
|
1149
|
+
"url_includes": {
|
|
1150
|
+
"type": "array",
|
|
1151
|
+
"items": { "type": "string" }
|
|
1152
|
+
},
|
|
1153
|
+
"methods": {
|
|
1154
|
+
"type": "array",
|
|
1155
|
+
"items": { "type": "string" }
|
|
1156
|
+
},
|
|
1157
|
+
"body_json_fields": {
|
|
1158
|
+
"type": "array",
|
|
1159
|
+
"items": { "type": "string" }
|
|
1160
|
+
},
|
|
1161
|
+
"max_requests": { "type": "integer" },
|
|
1162
|
+
"max_duration_ms": { "type": "integer" },
|
|
1163
|
+
"artifact_path": { "type": "string" }
|
|
1164
|
+
},
|
|
1165
|
+
"required": ["phase", "id"],
|
|
1166
|
+
"additionalProperties": false
|
|
1167
|
+
},
|
|
1168
|
+
"execution_capabilities": []
|
|
1169
|
+
},
|
|
1170
|
+
"app.network_assert": {
|
|
1171
|
+
"description": "Assert a previously indexed app.network_capture summary without hiding its diagnostics on failure.",
|
|
1172
|
+
"examples": [
|
|
1173
|
+
{
|
|
1174
|
+
"action": "app.network_assert",
|
|
1175
|
+
"id": "perps-home",
|
|
1176
|
+
"artifact_path": "network/perps-home.json",
|
|
1177
|
+
"required_status": "complete",
|
|
1178
|
+
"required_min_requests": 1,
|
|
1179
|
+
"required_types": ["allMids"],
|
|
1180
|
+
"forbidden_types": ["candleSnapshot"],
|
|
1181
|
+
"intent": "Assert the indexed request summary",
|
|
1182
|
+
"next": "done"
|
|
1183
|
+
}
|
|
1184
|
+
],
|
|
1185
|
+
"schema": {
|
|
1186
|
+
"type": "object",
|
|
1187
|
+
"properties": {
|
|
1188
|
+
"id": { "type": "string" },
|
|
1189
|
+
"artifact_path": { "type": "string" },
|
|
1190
|
+
"required_status": {
|
|
1191
|
+
"type": "string",
|
|
1192
|
+
"enum": ["complete", "partial", "unavailable"]
|
|
1193
|
+
},
|
|
1194
|
+
"required_min_requests": { "type": "integer" },
|
|
1195
|
+
"required_max_requests": { "type": "integer" },
|
|
1196
|
+
"required_types": {
|
|
1197
|
+
"type": "array",
|
|
1198
|
+
"items": { "type": "string" }
|
|
1199
|
+
},
|
|
1200
|
+
"forbidden_types": {
|
|
1201
|
+
"type": "array",
|
|
1202
|
+
"items": { "type": "string" }
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
"required": ["id"],
|
|
1206
|
+
"additionalProperties": false
|
|
1207
|
+
},
|
|
1208
|
+
"execution_capabilities": []
|
|
1209
|
+
},
|
|
1210
|
+
"app.performance_capture": {
|
|
1211
|
+
"description": "Capture bounded CDP renderer frame metrics and separately labelled JavaScript work across Recipe Protocol v1 node boundaries.",
|
|
1212
|
+
"examples": [
|
|
1213
|
+
{
|
|
1214
|
+
"action": "app.performance_capture",
|
|
1215
|
+
"phase": "start",
|
|
1216
|
+
"id": "homepage-scroll",
|
|
1217
|
+
"max_duration_ms": 300000,
|
|
1218
|
+
"intent": "Start the bounded UI smoothness window",
|
|
1219
|
+
"next": "exercise-flow"
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
"action": "app.performance_capture",
|
|
1223
|
+
"phase": "end",
|
|
1224
|
+
"id": "homepage-scroll",
|
|
1225
|
+
"artifact_path": "performance/homepage-scroll.json",
|
|
1226
|
+
"html_path": "performance/homepage-scroll.html",
|
|
1227
|
+
"intent": "End and index the UI smoothness window",
|
|
1228
|
+
"next": "assert-performance"
|
|
1229
|
+
}
|
|
1230
|
+
],
|
|
1231
|
+
"schema": {
|
|
1232
|
+
"type": "object",
|
|
1233
|
+
"properties": {
|
|
1234
|
+
"phase": {
|
|
1235
|
+
"type": "string",
|
|
1236
|
+
"enum": ["start", "end"]
|
|
1237
|
+
},
|
|
1238
|
+
"id": { "type": "string" },
|
|
1239
|
+
"max_duration_ms": {
|
|
1240
|
+
"type": "integer",
|
|
1241
|
+
"minimum": 1,
|
|
1242
|
+
"description": "Maximum accepted at runtime: 3600000 ms."
|
|
1243
|
+
},
|
|
1244
|
+
"artifact_path": { "type": "string" },
|
|
1245
|
+
"html_path": { "type": "string" }
|
|
1246
|
+
},
|
|
1247
|
+
"required": ["phase", "id"],
|
|
1248
|
+
"additionalProperties": false
|
|
1249
|
+
},
|
|
1250
|
+
"execution_capabilities": ["host-read-export"]
|
|
1251
|
+
},
|
|
1252
|
+
"app.performance_assert": {
|
|
1253
|
+
"description": "Assert a previously indexed app.performance_capture summary without hiding unavailable or partial source coverage.",
|
|
1254
|
+
"examples": [
|
|
1255
|
+
{
|
|
1256
|
+
"action": "app.performance_assert",
|
|
1257
|
+
"id": "homepage-scroll",
|
|
1258
|
+
"artifact_path": "performance/homepage-scroll.json",
|
|
1259
|
+
"required_status": ["complete", "partial"],
|
|
1260
|
+
"minimum_frame_count": 1,
|
|
1261
|
+
"require_native_ui_when_supported": true,
|
|
1262
|
+
"required_node_ids": ["scroll-homepage"],
|
|
1263
|
+
"intent": "Assert the indexed UI smoothness summary",
|
|
1264
|
+
"next": "done"
|
|
1265
|
+
}
|
|
1266
|
+
],
|
|
1267
|
+
"schema": {
|
|
1268
|
+
"type": "object",
|
|
1269
|
+
"properties": {
|
|
1270
|
+
"id": { "type": "string" },
|
|
1271
|
+
"artifact_path": { "type": "string" },
|
|
1272
|
+
"required_status": {
|
|
1273
|
+
"type": "array",
|
|
1274
|
+
"items": {
|
|
1275
|
+
"type": "string",
|
|
1276
|
+
"enum": ["complete", "partial", "unavailable"]
|
|
1277
|
+
}
|
|
1278
|
+
},
|
|
1279
|
+
"minimum_frame_count": {
|
|
1280
|
+
"type": "integer",
|
|
1281
|
+
"minimum": 0,
|
|
1282
|
+
"description": "Minimum MetaMask renderer frame count. JavaScript task count cannot satisfy this assertion."
|
|
1283
|
+
},
|
|
1284
|
+
"require_native_ui_when_supported": { "type": "boolean" },
|
|
1285
|
+
"required_node_ids": {
|
|
1286
|
+
"type": "array",
|
|
1287
|
+
"items": { "type": "string" }
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
"required": ["id"],
|
|
1291
|
+
"additionalProperties": false
|
|
1292
|
+
},
|
|
1293
|
+
"execution_capabilities": ["host-read-export"]
|
|
1294
|
+
},
|
|
1122
1295
|
"app.status": {
|
|
1123
1296
|
"description": "Report the adapter's static status — platform, project root, resolved checkout shape, and headless compatibility mode (no live route or account).",
|
|
1124
1297
|
"examples": [
|
|
@@ -1112,6 +1112,210 @@
|
|
|
1112
1112
|
},
|
|
1113
1113
|
"execution_capabilities": ["app-mutation"]
|
|
1114
1114
|
},
|
|
1115
|
+
"app.network_capture": {
|
|
1116
|
+
"description": "Capture redacted HTTP requests between named Recipe Protocol v1 nodes through the adapter's persistent Network observer.",
|
|
1117
|
+
"examples": [
|
|
1118
|
+
{
|
|
1119
|
+
"action": "app.network_capture",
|
|
1120
|
+
"phase": "start",
|
|
1121
|
+
"id": "perps-home",
|
|
1122
|
+
"url_includes": ["api.hyperliquid.xyz/info"],
|
|
1123
|
+
"methods": ["POST"],
|
|
1124
|
+
"body_json_fields": ["type", "req.coin", "dex"],
|
|
1125
|
+
"intent": "Start the redacted request window",
|
|
1126
|
+
"next": "exercise-flow"
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
"action": "app.network_capture",
|
|
1130
|
+
"phase": "end",
|
|
1131
|
+
"id": "perps-home",
|
|
1132
|
+
"artifact_path": "network/perps-home.json",
|
|
1133
|
+
"intent": "End and index the redacted request window",
|
|
1134
|
+
"next": "assert-network"
|
|
1135
|
+
}
|
|
1136
|
+
],
|
|
1137
|
+
"schema": {
|
|
1138
|
+
"type": "object",
|
|
1139
|
+
"properties": {
|
|
1140
|
+
"phase": {
|
|
1141
|
+
"type": "string",
|
|
1142
|
+
"enum": ["start", "end"]
|
|
1143
|
+
},
|
|
1144
|
+
"id": {
|
|
1145
|
+
"type": "string"
|
|
1146
|
+
},
|
|
1147
|
+
"url_includes": {
|
|
1148
|
+
"type": "array",
|
|
1149
|
+
"items": { "type": "string" }
|
|
1150
|
+
},
|
|
1151
|
+
"methods": {
|
|
1152
|
+
"type": "array",
|
|
1153
|
+
"items": { "type": "string" }
|
|
1154
|
+
},
|
|
1155
|
+
"body_json_fields": {
|
|
1156
|
+
"type": "array",
|
|
1157
|
+
"items": { "type": "string" }
|
|
1158
|
+
},
|
|
1159
|
+
"max_requests": {
|
|
1160
|
+
"type": "integer"
|
|
1161
|
+
},
|
|
1162
|
+
"max_duration_ms": {
|
|
1163
|
+
"type": "integer"
|
|
1164
|
+
},
|
|
1165
|
+
"artifact_path": {
|
|
1166
|
+
"type": "string"
|
|
1167
|
+
}
|
|
1168
|
+
},
|
|
1169
|
+
"required": ["phase", "id"],
|
|
1170
|
+
"additionalProperties": false
|
|
1171
|
+
},
|
|
1172
|
+
"execution_capabilities": []
|
|
1173
|
+
},
|
|
1174
|
+
"app.network_assert": {
|
|
1175
|
+
"description": "Assert a previously indexed app.network_capture summary without hiding its diagnostics on failure.",
|
|
1176
|
+
"examples": [
|
|
1177
|
+
{
|
|
1178
|
+
"action": "app.network_assert",
|
|
1179
|
+
"id": "perps-home",
|
|
1180
|
+
"artifact_path": "network/perps-home.json",
|
|
1181
|
+
"required_status": "complete",
|
|
1182
|
+
"required_min_requests": 1,
|
|
1183
|
+
"required_types": ["allMids"],
|
|
1184
|
+
"forbidden_types": ["candleSnapshot"],
|
|
1185
|
+
"intent": "Assert the indexed request summary",
|
|
1186
|
+
"next": "done"
|
|
1187
|
+
}
|
|
1188
|
+
],
|
|
1189
|
+
"schema": {
|
|
1190
|
+
"type": "object",
|
|
1191
|
+
"properties": {
|
|
1192
|
+
"id": {
|
|
1193
|
+
"type": "string"
|
|
1194
|
+
},
|
|
1195
|
+
"artifact_path": {
|
|
1196
|
+
"type": "string"
|
|
1197
|
+
},
|
|
1198
|
+
"required_status": {
|
|
1199
|
+
"type": "string",
|
|
1200
|
+
"enum": ["complete", "partial", "unavailable"]
|
|
1201
|
+
},
|
|
1202
|
+
"required_min_requests": {
|
|
1203
|
+
"type": "integer"
|
|
1204
|
+
},
|
|
1205
|
+
"required_max_requests": {
|
|
1206
|
+
"type": "integer"
|
|
1207
|
+
},
|
|
1208
|
+
"required_types": {
|
|
1209
|
+
"type": "array",
|
|
1210
|
+
"items": { "type": "string" }
|
|
1211
|
+
},
|
|
1212
|
+
"forbidden_types": {
|
|
1213
|
+
"type": "array",
|
|
1214
|
+
"items": { "type": "string" }
|
|
1215
|
+
}
|
|
1216
|
+
},
|
|
1217
|
+
"required": ["id"],
|
|
1218
|
+
"additionalProperties": false
|
|
1219
|
+
},
|
|
1220
|
+
"execution_capabilities": []
|
|
1221
|
+
},
|
|
1222
|
+
"app.performance_capture": {
|
|
1223
|
+
"description": "Capture bounded CDP native UI frame metrics and separately labelled JavaScript work across Recipe Protocol v1 node boundaries.",
|
|
1224
|
+
"examples": [
|
|
1225
|
+
{
|
|
1226
|
+
"action": "app.performance_capture",
|
|
1227
|
+
"phase": "start",
|
|
1228
|
+
"id": "homepage-scroll",
|
|
1229
|
+
"max_duration_ms": 300000,
|
|
1230
|
+
"intent": "Start the bounded UI smoothness window",
|
|
1231
|
+
"next": "exercise-flow"
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
"action": "app.performance_capture",
|
|
1235
|
+
"phase": "end",
|
|
1236
|
+
"id": "homepage-scroll",
|
|
1237
|
+
"artifact_path": "performance/homepage-scroll.json",
|
|
1238
|
+
"html_path": "performance/homepage-scroll.html",
|
|
1239
|
+
"intent": "End and index the UI smoothness window",
|
|
1240
|
+
"next": "assert-performance"
|
|
1241
|
+
}
|
|
1242
|
+
],
|
|
1243
|
+
"schema": {
|
|
1244
|
+
"type": "object",
|
|
1245
|
+
"properties": {
|
|
1246
|
+
"phase": {
|
|
1247
|
+
"type": "string",
|
|
1248
|
+
"enum": ["start", "end"]
|
|
1249
|
+
},
|
|
1250
|
+
"id": {
|
|
1251
|
+
"type": "string"
|
|
1252
|
+
},
|
|
1253
|
+
"max_duration_ms": {
|
|
1254
|
+
"type": "integer",
|
|
1255
|
+
"minimum": 1,
|
|
1256
|
+
"description": "Maximum accepted at runtime: 3600000 ms."
|
|
1257
|
+
},
|
|
1258
|
+
"artifact_path": {
|
|
1259
|
+
"type": "string"
|
|
1260
|
+
},
|
|
1261
|
+
"html_path": {
|
|
1262
|
+
"type": "string"
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
"required": ["phase", "id"],
|
|
1266
|
+
"additionalProperties": false
|
|
1267
|
+
},
|
|
1268
|
+
"execution_capabilities": ["host-read-export"]
|
|
1269
|
+
},
|
|
1270
|
+
"app.performance_assert": {
|
|
1271
|
+
"description": "Assert a previously indexed app.performance_capture summary without hiding unavailable or partial source coverage.",
|
|
1272
|
+
"examples": [
|
|
1273
|
+
{
|
|
1274
|
+
"action": "app.performance_assert",
|
|
1275
|
+
"id": "homepage-scroll",
|
|
1276
|
+
"artifact_path": "performance/homepage-scroll.json",
|
|
1277
|
+
"required_status": ["complete", "partial"],
|
|
1278
|
+
"minimum_frame_count": 1,
|
|
1279
|
+
"require_native_ui_when_supported": true,
|
|
1280
|
+
"required_node_ids": ["scroll-homepage"],
|
|
1281
|
+
"intent": "Assert the indexed UI smoothness summary",
|
|
1282
|
+
"next": "done"
|
|
1283
|
+
}
|
|
1284
|
+
],
|
|
1285
|
+
"schema": {
|
|
1286
|
+
"type": "object",
|
|
1287
|
+
"properties": {
|
|
1288
|
+
"id": {
|
|
1289
|
+
"type": "string"
|
|
1290
|
+
},
|
|
1291
|
+
"artifact_path": {
|
|
1292
|
+
"type": "string"
|
|
1293
|
+
},
|
|
1294
|
+
"required_status": {
|
|
1295
|
+
"type": "array",
|
|
1296
|
+
"items": {
|
|
1297
|
+
"type": "string",
|
|
1298
|
+
"enum": ["complete", "partial", "unavailable"]
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
"minimum_frame_count": {
|
|
1302
|
+
"type": "integer",
|
|
1303
|
+
"minimum": 0,
|
|
1304
|
+
"description": "Minimum native UI frame count. JavaScript task count cannot satisfy this assertion."
|
|
1305
|
+
},
|
|
1306
|
+
"require_native_ui_when_supported": {
|
|
1307
|
+
"type": "boolean"
|
|
1308
|
+
},
|
|
1309
|
+
"required_node_ids": {
|
|
1310
|
+
"type": "array",
|
|
1311
|
+
"items": { "type": "string" }
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
"required": ["id"],
|
|
1315
|
+
"additionalProperties": false
|
|
1316
|
+
},
|
|
1317
|
+
"execution_capabilities": ["host-read-export"]
|
|
1318
|
+
},
|
|
1115
1319
|
"app.status": {
|
|
1116
1320
|
"description": "Report the adapter's static status — platform, project root, resolved checkout shape, and headless compatibility mode (no live route or account).",
|
|
1117
1321
|
"examples": [
|