@lowdefy/server-dev 0.0.0-experimental-20261007090522 → 0.0.0-experimental-20261007124348
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/lib/docs/createDocsMcpServer.js +22 -10
- package/lib/docs/createDocsMcpServer.test.mjs +75 -0
- package/lib/docs/dataSets/resolveJourneyDataSet.js +11 -8
- package/lib/docs/devToolDefinitions.js +50 -18
- package/lib/docs/docs.test.mjs +23 -0
- package/lib/docs/evalOperator.js +3 -2
- package/lib/docs/evalOperatorHeadless.js +24 -1
- package/lib/docs/getSchema.js +29 -1
- package/lib/docs/inspectState.js +3 -2
- package/lib/docs/inspectStateHeadless.js +32 -3
- package/lib/docs/loadState.js +31 -16
- package/lib/docs/resolveSource.js +8 -6
- package/lib/docs/resolveSource.test.mjs +1 -1
- package/lib/docs/resolveToolCaller.js +62 -0
- package/lib/docs/runEndpoint.js +50 -9
- package/lib/docs/runEndpoint.test.mjs +96 -3
- package/lib/docs/runJourney.js +8 -3
- package/lib/docs/runJourney.test.mjs +27 -3
- package/lib/docs/runJourneySteps.js +26 -4
- package/lib/docs/runRequest.js +44 -30
- package/lib/docs/runRequest.test.mjs +2 -2
- package/lib/docs/screenshotPage.js +43 -23
- package/lib/docs/screenshotPage.test.mjs +110 -0
- package/lib/docs/withDataSession.js +53 -0
- package/lib/server/createLowdefyContext.js +9 -4
- package/lib/server/createLowdefyContext.test.mjs +15 -0
- package/lib/server/journeyCookies/forwardJourneyCookies.js +10 -2
- package/lib/server/journeyCookies.test.mjs +17 -4
- package/lib/server/mutants/groupMutantCopies.js +9 -2
- package/lib/server/mutants/listMutants.js +18 -2
- package/lib/server/mutants/listMutants.test.mjs +40 -0
- package/package.json +37 -37
- package/package.original.json +37 -37
- package/src/routes/docs/evalOperator.js +9 -1
- package/src/routes/docs/inspectState.js +8 -1
- package/src/routes/docs/journey.test.mjs +1 -1
- package/src/routes/docs/loadState.js +1 -1
- package/src/routes/docs/mutants.js +10 -1
- package/src/routes/docs/mutants.test.mjs +14 -0
- package/src/routes/docs/parseUserParam.js +27 -21
- package/src/routes/docs/parseUserParam.test.mjs +17 -5
- package/src/routes/docs/runEndpoint.js +2 -1
- package/src/routes/docs/runEndpoint.test.mjs +11 -1
- package/src/routes/docs/runRequest.js +2 -1
- package/src/routes/docs/runRequest.test.mjs +12 -2
- package/src/routes/docs/screenshot.js +1 -0
|
@@ -129,8 +129,8 @@ function createDocsMcpServer({ origin, honoContext, version } = {}) {
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
registerDevTool('lowdefy_inspect_state', async ({ pageId, pathParams, source, user }) => {
|
|
133
|
-
const result = await inspectState({ origin, pageId, pathParams, source, user });
|
|
132
|
+
registerDevTool('lowdefy_inspect_state', async ({ pageId, pathParams, source, user, data }) => {
|
|
133
|
+
const result = await inspectState({ origin, pageId, pathParams, source, user, data });
|
|
134
134
|
if (result.error) {
|
|
135
135
|
return notFoundResult(result.error);
|
|
136
136
|
}
|
|
@@ -139,8 +139,16 @@ function createDocsMcpServer({ origin, honoContext, version } = {}) {
|
|
|
139
139
|
|
|
140
140
|
registerDevTool(
|
|
141
141
|
'lowdefy_eval_operator',
|
|
142
|
-
async ({ pageId, pathParams, expression, source, user }) => {
|
|
143
|
-
const result = await evalOperator({
|
|
142
|
+
async ({ pageId, pathParams, expression, source, user, data }) => {
|
|
143
|
+
const result = await evalOperator({
|
|
144
|
+
origin,
|
|
145
|
+
pageId,
|
|
146
|
+
pathParams,
|
|
147
|
+
expression,
|
|
148
|
+
source,
|
|
149
|
+
user,
|
|
150
|
+
data,
|
|
151
|
+
});
|
|
144
152
|
if (result.error) {
|
|
145
153
|
return notFoundResult(result.error);
|
|
146
154
|
}
|
|
@@ -150,15 +158,17 @@ function createDocsMcpServer({ origin, honoContext, version } = {}) {
|
|
|
150
158
|
|
|
151
159
|
registerDevTool(
|
|
152
160
|
'lowdefy_run_request',
|
|
153
|
-
async ({ pageId, requestId, payload, user, saveResponse }) =>
|
|
154
|
-
textResult(
|
|
161
|
+
async ({ pageId, requestId, payload, user, data, saveResponse }) =>
|
|
162
|
+
textResult(
|
|
163
|
+
await runRequest({ pageId, requestId, payload, user, data, saveResponse, honoContext })
|
|
164
|
+
)
|
|
155
165
|
);
|
|
156
166
|
|
|
157
167
|
registerDevTool(
|
|
158
168
|
'lowdefy_run_endpoint',
|
|
159
|
-
async ({ endpointId, payload, user, system, saveResponse }) =>
|
|
169
|
+
async ({ endpointId, payload, user, data, system, saveResponse }) =>
|
|
160
170
|
textResult(
|
|
161
|
-
await runEndpoint({ endpointId, payload, user, system, saveResponse, honoContext })
|
|
171
|
+
await runEndpoint({ endpointId, payload, user, data, system, saveResponse, honoContext })
|
|
162
172
|
)
|
|
163
173
|
);
|
|
164
174
|
|
|
@@ -190,8 +200,8 @@ function createDocsMcpServer({ origin, honoContext, version } = {}) {
|
|
|
190
200
|
}
|
|
191
201
|
);
|
|
192
202
|
|
|
193
|
-
registerDevTool('lowdefy_load_state', async ({ name, mode, user }) => {
|
|
194
|
-
const result = await loadState({ origin, name, mode, user });
|
|
203
|
+
registerDevTool('lowdefy_load_state', async ({ name, mode, user, data }) => {
|
|
204
|
+
const result = await loadState({ origin, name, mode, user, data });
|
|
195
205
|
if (result.error) {
|
|
196
206
|
return notFoundResult(result.error);
|
|
197
207
|
}
|
|
@@ -257,6 +267,7 @@ function createDocsMcpServer({ origin, honoContext, version } = {}) {
|
|
|
257
267
|
scrollX,
|
|
258
268
|
scrollY,
|
|
259
269
|
user,
|
|
270
|
+
data,
|
|
260
271
|
width,
|
|
261
272
|
height,
|
|
262
273
|
colorScheme,
|
|
@@ -275,6 +286,7 @@ function createDocsMcpServer({ origin, honoContext, version } = {}) {
|
|
|
275
286
|
scrollX,
|
|
276
287
|
scrollY,
|
|
277
288
|
user,
|
|
289
|
+
data,
|
|
278
290
|
width,
|
|
279
291
|
height,
|
|
280
292
|
colorScheme,
|
|
@@ -197,6 +197,48 @@ test('MCP tools that render a page headless advertise an optional user parameter
|
|
|
197
197
|
await client.close();
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
+
test('MCP tools that act as someone take one user option and share one sentence explaining it', async () => {
|
|
201
|
+
const client = await connectClient();
|
|
202
|
+
const { tools } = await client.listTools();
|
|
203
|
+
const sentence =
|
|
204
|
+
'Pass user to act as someone: a user object such as {"roles":["admin"]}, "none" to act signed out, or the name of a user in the data set that data names';
|
|
205
|
+
|
|
206
|
+
[
|
|
207
|
+
'lowdefy_screenshot_page',
|
|
208
|
+
'lowdefy_run_journey',
|
|
209
|
+
'lowdefy_inspect_state',
|
|
210
|
+
'lowdefy_eval_operator',
|
|
211
|
+
'lowdefy_load_state',
|
|
212
|
+
'lowdefy_run_request',
|
|
213
|
+
'lowdefy_run_endpoint',
|
|
214
|
+
].forEach((name) => {
|
|
215
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
216
|
+
expect(tool.description).toContain(sentence);
|
|
217
|
+
expect(tool.inputSchema.properties.data.type).toEqual('string');
|
|
218
|
+
const forms = JSON.stringify(tool.inputSchema.properties.user);
|
|
219
|
+
expect(forms).toContain('"const":"none"');
|
|
220
|
+
expect(forms).toContain('"type":"string"');
|
|
221
|
+
expect(forms).toContain('"type":"object"');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await client.close();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('MCP tools/call lowdefy_screenshot_page passes data and a data set user name through', async () => {
|
|
228
|
+
mockScreenshotPage.mockResolvedValue({ data: 'AAAA', mimeType: 'image/png', screenshots: [] });
|
|
229
|
+
const client = await connectClient();
|
|
230
|
+
|
|
231
|
+
await client.callTool({
|
|
232
|
+
name: 'lowdefy_screenshot_page',
|
|
233
|
+
arguments: { pageId: 'tickets', user: 'member', data: 'crm' },
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(mockScreenshotPage).toHaveBeenCalledWith(
|
|
237
|
+
expect.objectContaining({ pageId: 'tickets', user: 'member', data: 'crm' })
|
|
238
|
+
);
|
|
239
|
+
await client.close();
|
|
240
|
+
});
|
|
241
|
+
|
|
200
242
|
test('MCP lowdefy_screenshot_page advertises width and height up to 4096, and colorScheme', async () => {
|
|
201
243
|
const client = await connectClient();
|
|
202
244
|
const { tools } = await client.listTools();
|
|
@@ -381,6 +423,39 @@ test('MCP tools/call with an unknown type returns isError with guidance', async
|
|
|
381
423
|
await client.close();
|
|
382
424
|
});
|
|
383
425
|
|
|
426
|
+
test('MCP lowdefy_get_schema returns the journey step grammar for kind journey-step without a type', async () => {
|
|
427
|
+
const client = await connectClient();
|
|
428
|
+
const { tools } = await client.listTools();
|
|
429
|
+
const getSchemaTool = tools.find((tool) => tool.name === 'lowdefy_get_schema');
|
|
430
|
+
expect(getSchemaTool.inputSchema.properties.kind.enum).toContain('journey-step');
|
|
431
|
+
expect(getSchemaTool.inputSchema.required).toEqual(['kind']);
|
|
432
|
+
const runJourneyTool = tools.find((tool) => tool.name === 'lowdefy_run_journey');
|
|
433
|
+
expect(runJourneyTool.inputSchema.properties.steps.description).toContain(
|
|
434
|
+
'lowdefy_get_schema with kind "journey-step"'
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const all = await client.callTool({
|
|
438
|
+
name: 'lowdefy_get_schema',
|
|
439
|
+
arguments: { kind: 'journey-step' },
|
|
440
|
+
});
|
|
441
|
+
expect(all.isError).toBeUndefined();
|
|
442
|
+
expect(JSON.parse(all.content[0].text).schema.title).toEqual('Journey step');
|
|
443
|
+
|
|
444
|
+
const wait = await client.callTool({
|
|
445
|
+
name: 'lowdefy_get_schema',
|
|
446
|
+
arguments: { kind: 'journey-step', type: 'wait' },
|
|
447
|
+
});
|
|
448
|
+
expect(JSON.parse(wait.content[0].text)).toMatchObject({ kind: 'journey-step', type: 'wait' });
|
|
449
|
+
|
|
450
|
+
const noType = await client.callTool({
|
|
451
|
+
name: 'lowdefy_get_schema',
|
|
452
|
+
arguments: { kind: 'blocks' },
|
|
453
|
+
});
|
|
454
|
+
expect(noType.isError).toBe(true);
|
|
455
|
+
expect(noType.content[0].text).toContain('requires a "type"');
|
|
456
|
+
await client.close();
|
|
457
|
+
});
|
|
458
|
+
|
|
384
459
|
// The dev server keeps serving the previous build when a rebuild fails, so
|
|
385
460
|
// every tool result must announce that its answer predates the caller's edits.
|
|
386
461
|
test('MCP tools/call prepends a STALE notice while the last build failed', async () => {
|
|
@@ -43,9 +43,13 @@ function describeDeclaredUsers({ users }) {
|
|
|
43
43
|
// which is bound to the real auth database while its requests read the data set;
|
|
44
44
|
// - any data-set journey while a dev mock user is active: the mock user wins over the injected
|
|
45
45
|
// caller, so the journey would silently act as someone outside the data set.
|
|
46
|
+
//
|
|
47
|
+
// `subject` names the run in the refusals: "journey" for a journey, "call" for the other dev tools
|
|
48
|
+
// that act as someone (resolveToolCaller), which take the same user and data.
|
|
46
49
|
async function resolveJourneyDataSet({
|
|
47
50
|
data,
|
|
48
51
|
user,
|
|
52
|
+
subject = 'journey',
|
|
49
53
|
configDirectory,
|
|
50
54
|
buildDirectory,
|
|
51
55
|
authConfigured,
|
|
@@ -54,31 +58,30 @@ async function resolveJourneyDataSet({
|
|
|
54
58
|
if (type.isNone(data)) {
|
|
55
59
|
if (user === 'none' && mockUserActive) {
|
|
56
60
|
return {
|
|
57
|
-
error:
|
|
58
|
-
'The journey has user "none", which cannot run while a dev mock user is active (auth.dev.mockUser or LOWDEFY_DEV_USER): with no caller of its own, every request would act as the mock user, not signed out. Remove the mock user to run journeys signed out, or give the journey a user.',
|
|
61
|
+
error: `The ${subject} has user "none", which cannot run while a dev mock user is active (auth.dev.mockUser or LOWDEFY_DEV_USER): with no caller of its own, every request would act as the mock user, not signed out. Remove the mock user to run signed out, or give the ${subject} a user.`,
|
|
59
62
|
};
|
|
60
63
|
}
|
|
61
64
|
if (isUserName(user)) {
|
|
62
65
|
return {
|
|
63
|
-
error: `The
|
|
66
|
+
error: `The ${subject}'s user ${JSON.stringify(
|
|
64
67
|
user
|
|
65
|
-
)} names a data set user, but the
|
|
68
|
+
)} names a data set user, but the ${subject} has no "data". Add data: <data set name>, or give user as an object.`,
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
71
|
return { user };
|
|
69
72
|
}
|
|
70
73
|
if (mockUserActive) {
|
|
71
74
|
return {
|
|
72
|
-
error: `The
|
|
75
|
+
error: `The ${subject} on data set ${JSON.stringify(
|
|
73
76
|
data
|
|
74
|
-
)} cannot run while a dev mock user is active (auth.dev.mockUser or LOWDEFY_DEV_USER): every request would act as the mock user, who is not in the data set. Remove the mock user to run
|
|
77
|
+
)} cannot run while a dev mock user is active (auth.dev.mockUser or LOWDEFY_DEV_USER): every request would act as the mock user, who is not in the data set. Remove the mock user to run on data sets.`,
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
80
|
if (user === 'none' && authConfigured) {
|
|
78
81
|
return {
|
|
79
|
-
error: `The
|
|
82
|
+
error: `The ${subject} on data set ${JSON.stringify(
|
|
80
83
|
data
|
|
81
|
-
)} has user "none", which is refused while auth is configured: signing in through the app would write to the app's real auth database. Name a data set user instead;
|
|
84
|
+
)} has user "none", which is refused while auth is configured: signing in through the app would write to the app's real auth database. Name a data set user instead; runs that sign in stay off data sets.`,
|
|
82
85
|
};
|
|
83
86
|
}
|
|
84
87
|
let dataSet;
|
|
@@ -37,7 +37,7 @@ Live state: lowdefy_inspect_state reads the ACTUAL state, request results, and e
|
|
|
37
37
|
|
|
38
38
|
Behaviour, not just layout: a screenshot shows what rendered, not what works. To verify behaviour, drive the page with lowdefy_run_journey — a declarative list of steps (click, fill, select, press, back, goto, email, as, wait, screenshot, expect) addressed by blockId — and assert on state, visibility, text, url or title. A failing step stops the journey and comes back as data (passed: false, failure with expected/actual, the remaining steps skipped) together with the final page state, so you can read what the app actually did and write the next assertion. A large final state comes back as a summary of its keys; pass state with the paths you need. Pass user to act as a real member (e.g. {"roles":["admin"]}) when the flow is role-gated, or user "none" to test auth itself (sign-up, sign-in, invitations) through the app's own pages and sessions.
|
|
39
39
|
|
|
40
|
-
Role-gated pages: the headless renderer signs in as a roleless user, so a page or request gated on a role renders empty or refused. Pass user to lowdefy_screenshot_page, lowdefy_run_journey, lowdefy_inspect_state, lowdefy_eval_operator, lowdefy_load_state, lowdefy_run_request or lowdefy_run_endpoint to act as a specific caller
|
|
40
|
+
Role-gated pages: the headless renderer signs in as a roleless user, so a page or request gated on a role renders empty or refused. Pass user to lowdefy_screenshot_page, lowdefy_run_journey, lowdefy_inspect_state, lowdefy_eval_operator, lowdefy_load_state, lowdefy_run_request or lowdefy_run_endpoint to act as a specific caller, in the same forms on every one: a user object (e.g. {"roles":["admin"]}), "none" to act signed out, or the name of a data set user together with data (e.g. user "member", data "crm"), which runs the call on that data set's own database. Vary it per call to compare what different roles see. A request run without user runs as a roleless anonymous caller, so a tenant-walled or role-gated request returns empty rather than an error.
|
|
41
41
|
|
|
42
42
|
Safety: lowdefy_checkpoint snapshots the config files before risky multi-file changes; lowdefy_revert_checkpoint restores them.
|
|
43
43
|
|
|
@@ -48,15 +48,27 @@ State checkpoints (testing): lowdefy_snapshot_state captures a page's live state
|
|
|
48
48
|
const HAZARDS_NOTE =
|
|
49
49
|
' Results include `hazards`: behaviours of this type that its schema does not show. Read them before writing config.';
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
51
|
+
// One sentence, shared by the description of every tool that acts as someone,
|
|
52
|
+
// so the caller option reads the same wherever an agent meets it.
|
|
53
|
+
const CALLER_NOTE =
|
|
54
|
+
' Pass user to act as someone: a user object such as {"roles":["admin"]}, "none" to act signed out, or the name of a user in the data set that data names (tests/data/<name>.yaml), which also runs the call on that data set\'s own database.';
|
|
55
|
+
|
|
56
|
+
// Shared by every tool that acts as someone, in the forms a journey takes, so
|
|
57
|
+
// one call can act as an admin and the next as a plain member - each headless
|
|
58
|
+
// call gets its own browser context, so they never share an identity.
|
|
54
59
|
const userSchema = z
|
|
55
|
-
.object({})
|
|
56
|
-
.
|
|
60
|
+
.union([z.literal('none'), z.string().min(1), z.object({}).passthrough()])
|
|
61
|
+
.optional()
|
|
62
|
+
.describe(
|
|
63
|
+
'Who the call acts as. Omitted: the default roleless headless user. An object: that injected caller, e.g. {"roles":["user-admin"]}, merged over the default, so include email/profile/attributes fields too if the page reads them; no auth engine runs for an injected caller, so nothing derives them. "none": no injected caller, so the app\'s own auth decides and the call runs signed out. Any other string: the name of a user in the data set named by data, e.g. "member". Headless only: it is never applied to a page the developer opens in their own browser, so combining it with source "tab" or load_state mode "registry-only" is an error rather than a silently dropped role.'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Shared by every tool that takes userSchema.
|
|
67
|
+
const dataSchema = z
|
|
68
|
+
.string()
|
|
57
69
|
.optional()
|
|
58
70
|
.describe(
|
|
59
|
-
'
|
|
71
|
+
'The name of a data set in tests/data/<name>.yaml. The call then runs against a fresh in-memory database of its own, loaded with the data set, as a journey with data does, and user can name one of its users. Headless only, like user.'
|
|
60
72
|
);
|
|
61
73
|
|
|
62
74
|
// Shared by every tool that names a page instance: a page whose `path` has
|
|
@@ -79,7 +91,8 @@ const saveResponseSchema = z
|
|
|
79
91
|
const devToolDefinitions = {
|
|
80
92
|
lowdefy_inspect_state: {
|
|
81
93
|
description:
|
|
82
|
-
"Read the LIVE state of a running page: state, request results, event log (recent actions fired), global, user, input, and urlQuery. If the developer has the page open in a browser it reads their actual tab (ask them to interact first, then inspect); otherwise it runs the page headless. Use this to see what the app's data model really looks like."
|
|
94
|
+
"Read the LIVE state of a running page: state, request results, event log (recent actions fired), global, user, input, and urlQuery. If the developer has the page open in a browser it reads their actual tab (ask them to interact first, then inspect); otherwise it runs the page headless. Use this to see what the app's data model really looks like." +
|
|
95
|
+
CALLER_NOTE,
|
|
83
96
|
inputSchema: {
|
|
84
97
|
pageId: z.string().describe('The page id to inspect.'),
|
|
85
98
|
pathParams: pathParamsSchema.describe(
|
|
@@ -90,12 +103,14 @@ const devToolDefinitions = {
|
|
|
90
103
|
.optional()
|
|
91
104
|
.describe('Force a source. Default: live tab if connected, else headless.'),
|
|
92
105
|
user: userSchema,
|
|
106
|
+
data: dataSchema,
|
|
93
107
|
},
|
|
94
108
|
},
|
|
95
109
|
|
|
96
110
|
lowdefy_eval_operator: {
|
|
97
111
|
description:
|
|
98
|
-
'Evaluate a Lowdefy operator expression against the live state of a running page — a REPL for config. Pass the operator object in the "expression" argument — any JSON value, e.g. {"_state": "customer.name"} or {"_if": {...}}. Evaluates in the real browser runtime (live tab if connected, else headless).'
|
|
112
|
+
'Evaluate a Lowdefy operator expression against the live state of a running page — a REPL for config. Pass the operator object in the "expression" argument — any JSON value, e.g. {"_state": "customer.name"} or {"_if": {...}}. Evaluates in the real browser runtime (live tab if connected, else headless).' +
|
|
113
|
+
CALLER_NOTE,
|
|
99
114
|
inputSchema: {
|
|
100
115
|
pageId: z.string().describe('The page id whose context to evaluate against.'),
|
|
101
116
|
pathParams: pathParamsSchema.describe(
|
|
@@ -106,28 +121,33 @@ const devToolDefinitions = {
|
|
|
106
121
|
.describe('The operator expression — any JSON value, e.g. {"_state": "key"}.'),
|
|
107
122
|
source: z.enum(['tab', 'headless']).optional(),
|
|
108
123
|
user: userSchema,
|
|
124
|
+
data: dataSchema,
|
|
109
125
|
},
|
|
110
126
|
},
|
|
111
127
|
|
|
112
128
|
lowdefy_run_request: {
|
|
113
129
|
description:
|
|
114
|
-
'Execute a request in dev with a test payload to verify the data shape a page receives. The page is built first when it changed since its last build, so the request that runs is the one in the config now; a page that fails to build is refused with its build errors. Read-only request types always run; write requests are refused unless the app opts in (cli.agentTools.allowWriteRequests in lowdefy.yaml). A response too large to return inline is written in full to responseFile.'
|
|
130
|
+
'Execute a request in dev with a test payload to verify the data shape a page receives. The page is built first when it changed since its last build, so the request that runs is the one in the config now; a page that fails to build is refused with its build errors. Read-only request types always run; write requests are refused unless the app opts in (cli.agentTools.allowWriteRequests in lowdefy.yaml). A response too large to return inline is written in full to responseFile.' +
|
|
131
|
+
CALLER_NOTE,
|
|
115
132
|
inputSchema: {
|
|
116
133
|
pageId: z.string().describe('The page the request is defined on.'),
|
|
117
134
|
requestId: z.string().describe('The request id.'),
|
|
118
135
|
payload: z.record(z.any()).optional().describe('Test payload for _payload operators.'),
|
|
119
136
|
user: userSchema,
|
|
137
|
+
data: dataSchema,
|
|
120
138
|
saveResponse: saveResponseSchema,
|
|
121
139
|
},
|
|
122
140
|
},
|
|
123
141
|
|
|
124
142
|
lowdefy_run_endpoint: {
|
|
125
143
|
description:
|
|
126
|
-
'Execute an Api endpoint routine in dev with a test payload and caller, to verify what it returns, rejects or throws. Requires agent write access (cli.agentTools.allowWriteRequests) because routines are not classified read-only. A :reject or :throw comes back as data (success: false, status "reject"/"error" with the routine\'s own error), not as a tool failure. Pass system: true to run it as a system context the way a cron or detached run does — no user (_user undefined), endpoint auth not checked, InternalApi endpoints allowed — which is the local test path for scheduled (schedules) and detached-only routines. Nested CallApi steps with detached: true still dispatch over HTTP and need CRON_SECRET set on the dev server; they are not faked.'
|
|
144
|
+
'Execute an Api endpoint routine in dev with a test payload and caller, to verify what it returns, rejects or throws. Requires agent write access (cli.agentTools.allowWriteRequests) because routines are not classified read-only. A :reject or :throw comes back as data (success: false, status "reject"/"error" with the routine\'s own error), not as a tool failure. Pass system: true to run it as a system context the way a cron or detached run does — no user (_user undefined), endpoint auth not checked, InternalApi endpoints allowed — which is the local test path for scheduled (schedules) and detached-only routines. Nested CallApi steps with detached: true still dispatch over HTTP and need CRON_SECRET set on the dev server; they are not faked.' +
|
|
145
|
+
CALLER_NOTE,
|
|
127
146
|
inputSchema: {
|
|
128
147
|
endpointId: z.string().describe('The Api endpoint id.'),
|
|
129
148
|
payload: z.record(z.any()).optional().describe('Test payload for _payload operators.'),
|
|
130
149
|
user: userSchema,
|
|
150
|
+
data: dataSchema,
|
|
131
151
|
system: z
|
|
132
152
|
.boolean()
|
|
133
153
|
.optional()
|
|
@@ -169,11 +189,13 @@ const devToolDefinitions = {
|
|
|
169
189
|
|
|
170
190
|
lowdefy_load_state: {
|
|
171
191
|
description:
|
|
172
|
-
"Put the app back into a saved state checkpoint. mode 'headless' (default) verifies the restored state itself; mode 'registry-only' loads the recorded request data into the dev server and returns a ?_checkpoint URL the developer can open to manually test the app in that exact state."
|
|
192
|
+
"Put the app back into a saved state checkpoint. mode 'headless' (default) verifies the restored state itself; mode 'registry-only' loads the recorded request data into the dev server and returns a ?_checkpoint URL the developer can open to manually test the app in that exact state." +
|
|
193
|
+
CALLER_NOTE,
|
|
173
194
|
inputSchema: {
|
|
174
195
|
name: z.string().describe('The checkpoint name.'),
|
|
175
196
|
mode: z.enum(['headless', 'registry-only']).optional(),
|
|
176
197
|
user: userSchema,
|
|
198
|
+
data: dataSchema,
|
|
177
199
|
},
|
|
178
200
|
},
|
|
179
201
|
|
|
@@ -250,7 +272,8 @@ const devToolDefinitions = {
|
|
|
250
272
|
|
|
251
273
|
lowdefy_screenshot_page: {
|
|
252
274
|
description:
|
|
253
|
-
'Screenshot a page of the running dev server (headless Chromium) to visually verify layout and rendering. Returns a PNG image. Set width (and height) to check a narrow or phone layout, e.g. width 390, and colorScheme "dark" to check dark mode. To capture a state beyond the page as it loads — an OPEN Selector / MultipleSelector / AutoComplete / DateSelector (calendar) / Cascader / TreeSelector dropdown, a modal a button opens — pass steps, e.g. [{"open": "status"}]: they run after the page settles and before the capture. Popups antd renders in a portal are included, also with fullPage. Pass urlQuery for a page that reads _url_query, and pathParams for a page whose path has placeholders.'
|
|
275
|
+
'Screenshot a page of the running dev server (headless Chromium) to visually verify layout and rendering. Returns a PNG image. Set width (and height) to check a narrow or phone layout, e.g. width 390, and colorScheme "dark" to check dark mode. To capture a state beyond the page as it loads — an OPEN Selector / MultipleSelector / AutoComplete / DateSelector (calendar) / Cascader / TreeSelector dropdown, a modal a button opens — pass steps, e.g. [{"open": "status"}]: they run after the page settles and before the capture. Popups antd renders in a portal are included, also with fullPage. Pass urlQuery for a page that reads _url_query, and pathParams for a page whose path has placeholders.' +
|
|
276
|
+
CALLER_NOTE,
|
|
254
277
|
inputSchema: {
|
|
255
278
|
pageId: z.string().describe('The page id to screenshot.'),
|
|
256
279
|
pathParams: pathParamsSchema,
|
|
@@ -305,18 +328,20 @@ const devToolDefinitions = {
|
|
|
305
328
|
'The colour scheme the page\'s prefers-color-scheme reports. Default "light". An app that follows the system theme renders dark with "dark"; a darkMode fixed in the app config wins.'
|
|
306
329
|
),
|
|
307
330
|
user: userSchema,
|
|
331
|
+
data: dataSchema,
|
|
308
332
|
},
|
|
309
333
|
},
|
|
310
334
|
|
|
311
335
|
lowdefy_run_journey: {
|
|
312
336
|
description:
|
|
313
|
-
'Drive a page of the running dev server headless through declarative steps and assert what happens — the way to verify behaviour (a form submits, a modal opens, a filter works), not just layout. Blocks are addressed by blockId; a target object narrows to a grid row/cell ({"blockId": "grid", "row": 1, "column": "actions"}), to the control with exactly some text ({"blockId": "grid", "row": 1, "text": "Edit"}), or reaches portal-rendered controls page-wide by text alone ({"text": "OK"} for a confirm dialog or modal footer button, a menu item). A step that fails stops the journey and is returned as data (passed: false, failure with index/step/expected/actual/message, later steps "skipped") — never as a tool error. A journey also fails at the step that causes an app error its own browsers raised (an action failing with an error that is not a user error, an uncaught page error, a server error or 5xx from a request or endpoint): failure.kind "app-error" with errors [{kind, message, source, configKey, key}], or phase "open" when opening the page raised it. A failed Validate, a Throw action and 401/403 refusals never count. Where an error is the intended outcome, {"expect": {"error": text}} straight after the interaction claims its errors whose message contains text. Returns the final page state (whole when small, otherwise stateOmitted with its size and top-level keys; see the state param) and any screenshots taken (as images after the JSON text).'
|
|
337
|
+
'Drive a page of the running dev server headless through declarative steps and assert what happens — the way to verify behaviour (a form submits, a modal opens, a filter works), not just layout. Blocks are addressed by blockId; a target object narrows to a grid row/cell ({"blockId": "grid", "row": 1, "column": "actions"}), to the control with exactly some text ({"blockId": "grid", "row": 1, "text": "Edit"}), or reaches portal-rendered controls page-wide by text alone ({"text": "OK"} for a confirm dialog or modal footer button, a menu item). A step that fails stops the journey and is returned as data (passed: false, failure with index/step/expected/actual/message, later steps "skipped") — never as a tool error. A journey also fails at the step that causes an app error its own browsers raised (an action failing with an error that is not a user error, an uncaught page error, a server error or 5xx from a request or endpoint): failure.kind "app-error" with errors [{kind, message, source, configKey, key}], or phase "open" when opening the page raised it. A failed Validate, a Throw action and 401/403 refusals never count. Where an error is the intended outcome, {"expect": {"error": text}} straight after the interaction claims its errors whose message contains text. Returns the final page state (whole when small, otherwise stateOmitted with its size and top-level keys; see the state param) and any screenshots taken (as images after the JSON text).' +
|
|
338
|
+
CALLER_NOTE,
|
|
314
339
|
inputSchema: {
|
|
315
340
|
pageId: z.string().describe('The page id to open.'),
|
|
316
341
|
steps: z
|
|
317
342
|
.array(z.record(z.any()))
|
|
318
343
|
.describe(
|
|
319
|
-
'Ordered steps, one key each: {"click": target} (a target object may add "count": 2 or 3 for that many clicks in quick succession, a double click, before the runner settles) | {"open": target} (open an input\'s dropdown or picker popup - Selector, MultipleSelector, AutoComplete, DateSelector, Cascader... - and wait for it to show, so a following screenshot step captures it) | {"fill": {...target, "value"}} | {"fill": {...target, "fromEmail": {"to", "subject"?, "match"}}} (type text read from the newest email to that address, like the email step, without leaving the page: the first match of the regular expression "match", or its first capture group, e.g. "\\\\b\\\\d{6}\\\\b" for a one-time sign-in code) | {"select": {...target, "value"}} (option by exact text: a dropdown option, or a radio, button or segmented option in the block) | {"press": "Enter" | "Mod+k"} (Mod is Meta/Control per platform) | {"back": true} (the browser Back button) | {"goto": pageId | {"pageId", "pathParams", "urlQuery"}} (load an app page like a typed URL, its path built from pathParams, one string per placeholder of the page\'s path; a protected page may redirect to sign-in) | {"email": {"to", "subject"?}} (open the newest email to that address, subject containing the text, that arrived during this journey, waiting for it if needed; opening it again opens the same message; then click its links by text, e.g. {"click": {"text": "Verify email address"}}; needs the dev server started with LOWDEFY_DEV_SMTP_PORT and the app\'s SMTP connection pointed at 127.0.0.1 on that port) | {"as": name} (switch to another person, each with their own browser and cookies; the journey starts as "main", and a new name opens the journey\'s page) | {"wait": {"ms": n} | {"request": requestId} (a call started since the last interaction, or since the page opened, has finished) | {"state": path}} | {"screenshot": name?} | {"expect": {"state": {"path", "equals"}} | {"visible": target} | {"hidden": target} (nothing the target names is visible; passes at once when nothing matches yet, so pair it with something that must be present first) | {"text": {...target, "contains"}} | {"url": {"contains"}} | {"title": {"equals"} | {"contains"}} | {"calls": {"request", "pageId"?, "count"} | {"endpoint", "count"}} | {"error": text} | {"effect": true}} (title is the document title; calls compares, once the page settles, how many times this actor called the request - on pageId, default the current page - or the endpoint since the journey started; error must directly follow an interaction step and claims the app errors it raised whose message contains text, with the failed action that reported them; effect must directly follow an interaction step and fails when that interaction did nothing: no event ran, the page did not change, no request or endpoint was called and the URL stayed the same, which proves a dead click). A target is a blockId string, or an object of {"blockId", "row" (zero-based grid row as displayed), "column" (grid col-id), "text" (exact text of the interactive control to use), "containing" (text the element shows, e.g. the email a list row shows; a click on it reaches the row\'s click handler), "nth" (zero-based pick among several matches; a click, open, fill or select whose text or containing matches more than one visible element fails without it)}; "text" without "blockId" searches the whole page, which is how confirm dialog / modal footer buttons, dropdown menu items and email links are reached. fill, select and expect.text need a blockId. Every blockId, requestId, endpoint and goto pageId a step names is checked against the build before it runs (and an "as" name against the data set\'s users on a data set run): an unknown one fails the step and names it. fill, select and expect.state take an optional "from": "recorded" (the value came from a recorded trace) or "shape" (value: null, a placeholder a trace could not hold); a journey holding a from: shape placeholder is refused until it is filled in. Each step gets 5s, or the journey\'s timeout: every wait for something to happen (a control to be actionable, an expect to match, a request, state or email to arrive) is bounded by it; page opens get at least 15s; after an interaction the runner waits at most 5s for the page\'s pending events and requests to settle, without failing; wait.ms is exact.'
|
|
344
|
+
'Ordered steps, one key each: {"click": target} (a target object may add "count": 2 or 3 for that many clicks in quick succession, a double click, before the runner settles) | {"open": target} (open an input\'s dropdown or picker popup - Selector, MultipleSelector, AutoComplete, DateSelector, Cascader... - and wait for it to show, so a following screenshot step captures it) | {"fill": {...target, "value"}} | {"fill": {...target, "fromEmail": {"to", "subject"?, "match"}}} (type text read from the newest email to that address, like the email step, without leaving the page: the first match of the regular expression "match", or its first capture group, e.g. "\\\\b\\\\d{6}\\\\b" for a one-time sign-in code) | {"select": {...target, "value"}} (option by exact text: a dropdown option, or a radio, button or segmented option in the block) | {"press": "Enter" | "Mod+k"} (Mod is Meta/Control per platform) | {"back": true} (the browser Back button) | {"goto": pageId | {"pageId", "pathParams", "urlQuery"}} (load an app page like a typed URL, its path built from pathParams, one string per placeholder of the page\'s path; a protected page may redirect to sign-in) | {"email": {"to", "subject"?}} (open the newest email to that address, subject containing the text, that arrived during this journey, waiting for it if needed; opening it again opens the same message; then click its links by text, e.g. {"click": {"text": "Verify email address"}}; needs the dev server started with LOWDEFY_DEV_SMTP_PORT and the app\'s SMTP connection pointed at 127.0.0.1 on that port) | {"as": name} (switch to another person, each with their own browser and cookies; the journey starts as "main", and a new name opens the journey\'s page) | {"wait": {"ms": n} | {"request": requestId} (a call started since the last interaction, or since the page opened, has finished) | {"state": path}} | {"screenshot": name?} | {"expect": {"state": {"path", "equals"}} | {"visible": target} | {"hidden": target} (nothing the target names is visible; passes at once when nothing matches yet, so pair it with something that must be present first) | {"text": {...target, "contains"}} | {"url": {"contains"}} | {"title": {"equals"} | {"contains"}} | {"calls": {"request", "pageId"?, "count"} | {"endpoint", "count"}} | {"error": text} | {"effect": true}} (title is the document title; calls compares, once the page settles, how many times this actor called the request - on pageId, default the current page - or the endpoint since the journey started; error must directly follow an interaction step and claims the app errors it raised whose message contains text, with the failed action that reported them; effect must directly follow an interaction step and fails when that interaction did nothing: no event ran, the page did not change, no request or endpoint was called and the URL stayed the same, which proves a dead click). A target is a blockId string, or an object of {"blockId", "row" (zero-based grid row as displayed), "column" (grid col-id), "text" (exact text of the interactive control to use), "containing" (text the element shows, e.g. the email a list row shows; a click on it reaches the row\'s click handler), "nth" (zero-based pick among several matches; a click, open, fill or select whose text or containing matches more than one visible element fails without it)}; "text" without "blockId" searches the whole page, which is how confirm dialog / modal footer buttons, dropdown menu items and email links are reached. fill, select and expect.text need a blockId. Every blockId, requestId, endpoint and goto pageId a step names is checked against the build before it runs (and an "as" name against the data set\'s users on a data set run): an unknown one fails the step and names it. fill, select and expect.state take an optional "from": "recorded" (the value came from a recorded trace) or "shape" (value: null, a placeholder a trace could not hold); a journey holding a from: shape placeholder is refused until it is filled in. Each step gets 5s, or the journey\'s timeout: every wait for something to happen (a control to be actionable, an expect to match, a request, state or email to arrive) is bounded by it; page opens get at least 15s; after an interaction the runner waits at most 5s for the page\'s pending events and requests to settle, without failing; wait.ms is exact. lowdefy_get_schema with kind "journey-step" returns this grammar as a JSON schema, with examples of every step.'
|
|
320
345
|
),
|
|
321
346
|
user: z
|
|
322
347
|
.union([
|
|
@@ -409,9 +434,16 @@ const devToolDefinitions = {
|
|
|
409
434
|
HAZARDS_NOTE,
|
|
410
435
|
inputSchema: {
|
|
411
436
|
kind: z
|
|
412
|
-
.enum(['blocks', 'operators', 'actions', 'connections', 'requests'])
|
|
413
|
-
.describe(
|
|
414
|
-
|
|
437
|
+
.enum(['blocks', 'operators', 'actions', 'connections', 'requests', 'journey-step'])
|
|
438
|
+
.describe(
|
|
439
|
+
'The kind of the type. "journey-step" returns the lowdefy_run_journey step grammar, with examples of every step.'
|
|
440
|
+
),
|
|
441
|
+
type: z
|
|
442
|
+
.string()
|
|
443
|
+
.optional()
|
|
444
|
+
.describe(
|
|
445
|
+
'The exact type name, e.g. "Button", "_get", "MongoDBFind". Required, except for kind "journey-step", where it names one step (e.g. "wait") and is omitted for every step.'
|
|
446
|
+
),
|
|
415
447
|
},
|
|
416
448
|
},
|
|
417
449
|
|
package/lib/docs/docs.test.mjs
CHANGED
|
@@ -158,6 +158,29 @@ test('getSchema returns null for unknown type', () => {
|
|
|
158
158
|
|
|
159
159
|
test('getSchema throws for kinds without schemas', () => {
|
|
160
160
|
expect(() => getSchema({ kind: 'websockets', type: 'X' })).toThrow('No schemas available');
|
|
161
|
+
expect(() => getSchema({ kind: 'tool', type: 'run_journey' })).toThrow(
|
|
162
|
+
'or journey-step for the journey step grammar'
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('getSchema throws when a type kind is given no type', () => {
|
|
167
|
+
expect(() => getSchema({ kind: 'blocks' })).toThrow(
|
|
168
|
+
'Getting a blocks schema requires a "type", the exact type name. Received undefined.'
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('getSchema returns the journey step grammar for kind journey-step', () => {
|
|
173
|
+
const result = getSchema({ kind: 'journey-step' });
|
|
174
|
+
expect(result.kind).toEqual('journey-step');
|
|
175
|
+
expect(result.schema.title).toEqual('Journey step');
|
|
176
|
+
expect(result.schema.oneOf.map((step) => step.required[0])).toContain('wait');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('getSchema returns one journey step schema when type names the step', () => {
|
|
180
|
+
const result = getSchema({ kind: 'journey-step', type: 'wait' });
|
|
181
|
+
expect(result).toMatchObject({ kind: 'journey-step', type: 'wait' });
|
|
182
|
+
expect(result.schema.properties.wait.examples).toContainEqual({ wait: { ms: 500 } });
|
|
183
|
+
expect(getSchema({ kind: 'journey-step', type: 'hover' })).toBeNull();
|
|
161
184
|
});
|
|
162
185
|
|
|
163
186
|
test('getExamples reads example yaml from plugin dist by convention', () => {
|
package/lib/docs/evalOperator.js
CHANGED
|
@@ -22,13 +22,14 @@ import resolveSource from './resolveSource.js';
|
|
|
22
22
|
// headless-only. Falls back to headless whenever the tab path isn't usable
|
|
23
23
|
// (no tab connected, or `source: 'tab'` was requested but it errored) so
|
|
24
24
|
// agents always get an answer.
|
|
25
|
-
async function evalOperator({ origin, pageId, pathParams, expression, source, user }) {
|
|
25
|
+
async function evalOperator({ origin, pageId, pathParams, expression, source, user, data }) {
|
|
26
26
|
const { tryTab, error, invalidInput } = resolveSource({
|
|
27
27
|
name: 'evalOperator',
|
|
28
28
|
pageId,
|
|
29
29
|
pathParams,
|
|
30
30
|
source,
|
|
31
31
|
user,
|
|
32
|
+
data,
|
|
32
33
|
});
|
|
33
34
|
if (error) {
|
|
34
35
|
return { error, invalidInput };
|
|
@@ -41,7 +42,7 @@ async function evalOperator({ origin, pageId, pathParams, expression, source, us
|
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
const result = await evalOperatorHeadless({ origin, pageId, pathParams, expression, user });
|
|
45
|
+
const result = await evalOperatorHeadless({ origin, pageId, pathParams, expression, user, data });
|
|
45
46
|
return { ...result, source: 'headless' };
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -19,8 +19,10 @@ import { type } from '@lowdefy/helpers';
|
|
|
19
19
|
import { getBrowser, openPage, buildPageUrl } from './getBrowser.js';
|
|
20
20
|
import noBrowserError from './noBrowserError.js';
|
|
21
21
|
import resolvePageInstance from './resolvePageInstance.js';
|
|
22
|
+
import resolveToolCaller from './resolveToolCaller.js';
|
|
22
23
|
import unsettledPageNote from './unsettledPageNote.js';
|
|
23
24
|
import withBrowserSlot from './withBrowserSlot.js';
|
|
25
|
+
import withDataSession from './withDataSession.js';
|
|
24
26
|
|
|
25
27
|
// Evaluates an operator expression against the live client state of a
|
|
26
28
|
// headless Chromium tab navigated to the page instance's own route (`pathParams`
|
|
@@ -33,6 +35,7 @@ async function evalOperatorHeadless({
|
|
|
33
35
|
pathParams,
|
|
34
36
|
expression,
|
|
35
37
|
user,
|
|
38
|
+
data,
|
|
36
39
|
timeout = 15000,
|
|
37
40
|
}) {
|
|
38
41
|
if (type.isNone(origin) || !type.isString(origin)) {
|
|
@@ -56,9 +59,27 @@ async function evalOperatorHeadless({
|
|
|
56
59
|
return { error: instance.error, invalidInput: true };
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
const caller = await resolveToolCaller({ user, data });
|
|
63
|
+
if (!type.isUndefined(caller.error)) {
|
|
64
|
+
return caller;
|
|
65
|
+
}
|
|
66
|
+
|
|
59
67
|
return withBrowserSlot({
|
|
60
68
|
task: () =>
|
|
61
|
-
|
|
69
|
+
withDataSession({
|
|
70
|
+
dataSet: caller.dataSet,
|
|
71
|
+
task: ({ dataCookie }) =>
|
|
72
|
+
evalOperatorInBrowser({
|
|
73
|
+
origin,
|
|
74
|
+
pageId,
|
|
75
|
+
pathParams,
|
|
76
|
+
instance,
|
|
77
|
+
expression,
|
|
78
|
+
user: caller.user,
|
|
79
|
+
dataCookie,
|
|
80
|
+
timeout,
|
|
81
|
+
}),
|
|
82
|
+
}),
|
|
62
83
|
});
|
|
63
84
|
}
|
|
64
85
|
|
|
@@ -70,6 +91,7 @@ async function evalOperatorInBrowser({
|
|
|
70
91
|
instance,
|
|
71
92
|
expression,
|
|
72
93
|
user,
|
|
94
|
+
dataCookie,
|
|
73
95
|
timeout,
|
|
74
96
|
}) {
|
|
75
97
|
let browser;
|
|
@@ -90,6 +112,7 @@ async function evalOperatorInBrowser({
|
|
|
90
112
|
path: instance.path,
|
|
91
113
|
pathParams,
|
|
92
114
|
user,
|
|
115
|
+
dataCookie,
|
|
93
116
|
timeout,
|
|
94
117
|
});
|
|
95
118
|
context = opened.context;
|
package/lib/docs/getSchema.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { type } from '@lowdefy/helpers';
|
|
18
|
+
import { JOURNEY_STEP_SCHEMAS, journeyStepSchema } from '@lowdefy/node-utils';
|
|
18
19
|
|
|
19
20
|
import getHazards from './getHazards.js';
|
|
20
21
|
import normalizeTypeKind from './normalizeTypeKind.js';
|
|
@@ -28,13 +29,40 @@ const SCHEMA_ARTIFACTS = {
|
|
|
28
29
|
requests: 'plugins/requestSchemas.json',
|
|
29
30
|
};
|
|
30
31
|
|
|
32
|
+
const JOURNEY_STEP_KINDS = ['journey-step', 'journey-steps'];
|
|
33
|
+
|
|
34
|
+
// kind journey-step returns the journey step grammar: every step, or the one
|
|
35
|
+
// step `type` names.
|
|
36
|
+
function getJourneyStepSchema({ typeName }) {
|
|
37
|
+
if (type.isNone(typeName) || typeName === '' || typeName === 'all') {
|
|
38
|
+
return { kind: 'journey-step', schema: journeyStepSchema };
|
|
39
|
+
}
|
|
40
|
+
const schema = JOURNEY_STEP_SCHEMAS[typeName];
|
|
41
|
+
if (type.isNone(schema)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return { kind: 'journey-step', type: typeName, schema };
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
function getSchema({ kind, type: typeName }) {
|
|
48
|
+
if (JOURNEY_STEP_KINDS.includes(String(kind ?? '').toLowerCase())) {
|
|
49
|
+
return getJourneyStepSchema({ typeName });
|
|
50
|
+
}
|
|
32
51
|
const normalizedKind = normalizeTypeKind({ kind });
|
|
33
52
|
if (type.isNone(SCHEMA_ARTIFACTS[normalizedKind])) {
|
|
34
53
|
throw new Error(
|
|
35
54
|
`No schemas available for type kind. Received ${JSON.stringify(
|
|
36
55
|
kind
|
|
37
|
-
)}. Use one of: blocks, operators, actions, connections, requests.`
|
|
56
|
+
)}. Use one of: blocks, operators, actions, connections, requests, or journey-step for the journey step grammar.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
// type is optional only for journey-step, so a type kind without one is
|
|
60
|
+
// refused rather than looked up as "undefined".
|
|
61
|
+
if (type.isNone(typeName) || typeName === '') {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Getting a ${normalizedKind} schema requires a "type", the exact type name. Received ${JSON.stringify(
|
|
64
|
+
typeName
|
|
65
|
+
)}.`
|
|
38
66
|
);
|
|
39
67
|
}
|
|
40
68
|
const schemas = readBuildArtifact({ name: SCHEMA_ARTIFACTS[normalizedKind] }) ?? {};
|
package/lib/docs/inspectState.js
CHANGED
|
@@ -22,13 +22,14 @@ import resolveSource from './resolveSource.js';
|
|
|
22
22
|
// headless-only. Falls back to headless whenever the tab path isn't usable
|
|
23
23
|
// (no tab connected, or `source: 'tab'` was requested but it errored) so
|
|
24
24
|
// agents always get an answer.
|
|
25
|
-
async function inspectState({ origin, pageId, pathParams, source, user }) {
|
|
25
|
+
async function inspectState({ origin, pageId, pathParams, source, user, data }) {
|
|
26
26
|
const { tryTab, error, invalidInput } = resolveSource({
|
|
27
27
|
name: 'inspectState',
|
|
28
28
|
pageId,
|
|
29
29
|
pathParams,
|
|
30
30
|
source,
|
|
31
31
|
user,
|
|
32
|
+
data,
|
|
32
33
|
});
|
|
33
34
|
if (error) {
|
|
34
35
|
return { error, invalidInput };
|
|
@@ -41,7 +42,7 @@ async function inspectState({ origin, pageId, pathParams, source, user }) {
|
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
const result = await inspectStateHeadless({ origin, pageId, pathParams, user });
|
|
45
|
+
const result = await inspectStateHeadless({ origin, pageId, pathParams, user, data });
|
|
45
46
|
return { ...result, source: 'headless' };
|
|
46
47
|
}
|
|
47
48
|
|