@lowdefy/server-dev 5.6.0 → 6.1.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/{pages/_app.js → client/App.jsx} +21 -33
- package/client/DevStreamContext.js +30 -0
- package/client/Inspector.jsx +226 -0
- package/{lib/client/Page.js → client/Page.jsx} +4 -8
- package/client/Reload.jsx +85 -0
- package/client/Routing.jsx +117 -0
- package/client/clientBuildImports.test.mjs +61 -0
- package/client/feedback/FeedbackMount.jsx +83 -0
- package/client/feedback/FeedbackOverlay.jsx +901 -0
- package/client/feedback/captureTabScreenshot.js +84 -0
- package/client/feedback/copyToClipboard.js +45 -0
- package/client/feedback/drawAnnotationsSvg.js +119 -0
- package/client/feedback/elementInspect.js +99 -0
- package/client/feedback/feedbackStyles.js +301 -0
- package/client/feedback/pinResponsiveImages.js +59 -0
- package/client/feedback/sendFeedback.js +39 -0
- package/client/feedback/useFeedbackToggle.js +70 -0
- package/client/main.jsx +42 -0
- package/client/openInEditor/OpenInEditorListener.jsx +268 -0
- package/client/openInEditor/findBlockLocation.js +51 -0
- package/lib/build/app.js +8 -1
- package/lib/build/appMeta.js +8 -1
- package/lib/build/auth.js +8 -1
- package/lib/build/config.js +8 -1
- package/lib/build/i18n.js +8 -1
- package/lib/build/logger.js +8 -1
- package/lib/build/theme.js +8 -1
- package/lib/client/auth/{Auth.js → Auth.jsx} +11 -2
- package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +22 -10
- package/lib/client/setPageId.js +6 -5
- package/lib/client/utils/useMutateCache.js +14 -1
- package/lib/client/utils/usePageConfig.js +45 -43
- package/lib/client/utils/usePageConfig.test.mjs +62 -0
- package/lib/docs/captureAnnotatedScreenshot.js +88 -0
- package/lib/docs/checkpointPaths.js +91 -0
- package/lib/docs/checkpointStore.js +240 -0
- package/lib/docs/checkpointToMocks.js +65 -0
- package/lib/docs/clientErrorStore.js +36 -0
- package/lib/docs/configCheckpoints.test.mjs +156 -0
- package/lib/docs/createConfigCheckpoint.js +92 -0
- package/lib/docs/createDocsMcpServer.js +531 -0
- package/lib/docs/createDocsMcpServer.test.mjs +141 -0
- package/lib/docs/devMockRegistry.js +68 -0
- package/lib/docs/docs.test.mjs +448 -0
- package/lib/docs/enrichFeedback.js +71 -0
- package/lib/docs/evalOperator.js +47 -0
- package/lib/docs/evalOperator.test.mjs +157 -0
- package/lib/docs/evalOperatorHeadless.js +87 -0
- package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
- package/lib/docs/evalOperatorInTab.js +48 -0
- package/lib/docs/findConfig.js +260 -0
- package/lib/docs/formatFeedback.js +121 -0
- package/lib/docs/formatFeedback.test.mjs +204 -0
- package/lib/docs/getAppMap.js +180 -0
- package/lib/docs/getBrowser.js +127 -0
- package/lib/docs/getBrowser.test.mjs +122 -0
- package/lib/docs/getBuildStatus.js +37 -0
- package/lib/docs/getCoreDoc.js +63 -0
- package/lib/docs/getDocsManifest.js +40 -0
- package/lib/docs/getExamples.js +58 -0
- package/lib/docs/getOverview.js +115 -0
- package/lib/docs/getPageConfig.js +53 -0
- package/lib/docs/getPluginDoc.js +55 -0
- package/lib/docs/getSchema.js +65 -0
- package/lib/docs/inspectState.js +47 -0
- package/lib/docs/inspectState.test.mjs +140 -0
- package/lib/docs/inspectStateFromTab.js +40 -0
- package/lib/docs/inspectStateHeadless.js +84 -0
- package/lib/docs/inspectStateHeadless.test.mjs +60 -0
- package/lib/docs/isWriteRequestsAllowed.js +50 -0
- package/lib/docs/listConfigCheckpoints.js +55 -0
- package/lib/docs/listPlugins.js +80 -0
- package/lib/docs/listTypes.js +105 -0
- package/lib/docs/loadState.js +152 -0
- package/lib/docs/mapPageBuildErrors.js +28 -0
- package/lib/docs/normalizeTypeKind.js +40 -0
- package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
- package/lib/docs/readPageArtifact.js +26 -0
- package/lib/docs/resolvePluginDir.js +49 -0
- package/lib/docs/resolveSource.js +48 -0
- package/lib/docs/resolveSource.test.mjs +100 -0
- package/lib/docs/revertConfigCheckpoint.js +73 -0
- package/lib/docs/runRequest.js +141 -0
- package/lib/docs/saveAnnotatedScreenshot.js +43 -0
- package/lib/docs/scaffoldPage.js +75 -0
- package/lib/docs/scaffoldPage.test.mjs +97 -0
- package/lib/docs/screenshotPage.js +110 -0
- package/lib/docs/screenshotPage.test.mjs +59 -0
- package/lib/docs/screenshotPageClip.test.mjs +102 -0
- package/lib/docs/searchDocs.js +67 -0
- package/lib/docs/setupTestFixtures.mjs +335 -0
- package/lib/docs/snapshotState.js +59 -0
- package/lib/docs/stateCheckpoints.test.mjs +366 -0
- package/lib/docs/tabAvailable.js +28 -0
- package/lib/docs/tabChannel.js +119 -0
- package/lib/docs/tabChannel.test.mjs +130 -0
- package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +9 -4
- package/lib/server/auth/getDevSession.js +70 -0
- package/lib/server/auth/getDevSession.test.mjs +138 -0
- package/lib/server/auth/getHeadlessUser.js +40 -0
- package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
- package/lib/server/auth/headlessUser.js +29 -0
- package/lib/server/auth/resolveHeadlessUser.js +38 -0
- package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
- package/lib/server/auth/session.js +39 -0
- package/lib/server/auth/strategies.js +47 -0
- package/lib/server/createLowdefyContext.js +128 -0
- package/lib/server/createLowdefyContext.test.mjs +117 -0
- package/lib/server/getPageJitEnrichment.test.mjs +105 -0
- package/lib/server/jitPageBuilder.js +125 -12
- package/lib/server/log/createHandleError.test.mjs +126 -0
- package/lib/server/log/createLogger.test.mjs +62 -0
- package/lib/server/scrubSecrets.js +23 -0
- package/manager/getContext.mjs +3 -7
- package/manager/processes/initialBuild.mjs +0 -2
- package/manager/processes/installPlugins.mjs +19 -11
- package/manager/processes/lowdefyBuild.mjs +29 -7
- package/manager/processes/lowdefyBuild.test.mjs +102 -0
- package/manager/processes/readDotEnv.mjs +13 -1
- package/manager/processes/shutdownServer.mjs +6 -6
- package/manager/processes/startProxy.mjs +160 -0
- package/manager/processes/startProxy.test.mjs +102 -0
- package/manager/processes/startServer.mjs +33 -16
- package/manager/processes/startWatchers.mjs +2 -2
- package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
- package/manager/run.mjs +50 -0
- package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
- package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
- package/manager/utils/formatNoticeBox.mjs +30 -0
- package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
- package/manager/utils/updatePageTailwindCss.mjs +7 -1
- package/manager/utils/writeBuildStatus.mjs +39 -0
- package/manager/utils/writeBuildStatus.test.mjs +76 -0
- package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
- package/manager/watchers/moduleBuildWatcher.mjs +0 -1
- package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
- package/package.json +62 -49
- package/package.original.json +65 -52
- package/postcss.config.cjs +1 -0
- package/src/app.js +175 -0
- package/src/html/renderDevPage.js +86 -0
- package/src/lib/getPathSegments.js +25 -0
- package/src/lib/safeScriptJson.js +38 -0
- package/src/middleware/apiContext.js +32 -0
- package/src/middleware/errorHandler.js +48 -0
- package/src/middleware/errorHandler.test.mjs +247 -0
- package/src/routes/agent.js +64 -0
- package/src/routes/apiPage.js +37 -0
- package/src/routes/auth.js +49 -0
- package/src/routes/auth.test.mjs +84 -0
- package/src/routes/clientError.js +57 -0
- package/src/routes/cron.js +47 -0
- package/src/routes/cronForward.js +47 -0
- package/src/routes/detached.js +47 -0
- package/src/routes/devInspect.js +111 -0
- package/src/routes/devInspect.test.mjs +119 -0
- package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
- package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
- package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
- package/src/routes/docs/checkpointsCreate.js +28 -0
- package/src/routes/docs/checkpointsList.js +23 -0
- package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
- package/src/routes/docs/content.js +31 -0
- package/src/routes/docs/evalOperator.js +56 -0
- package/src/routes/docs/examples.js +33 -0
- package/src/routes/docs/find.js +30 -0
- package/src/routes/docs/index.js +23 -0
- package/src/routes/docs/inspectState.js +44 -0
- package/src/routes/docs/loadState.js +49 -0
- package/src/routes/docs/mcp.js +30 -0
- package/src/routes/docs/pageConfig.js +36 -0
- package/src/routes/docs/parseUserParam.js +52 -0
- package/src/routes/docs/parseUserParam.test.mjs +45 -0
- package/src/routes/docs/pluginDoc.js +33 -0
- package/src/routes/docs/plugins.js +23 -0
- package/src/routes/docs/runRequest.js +36 -0
- package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
- package/src/routes/docs/screenshot.js +72 -0
- package/src/routes/docs/search.js +27 -0
- package/src/routes/docs/snapshotState.js +36 -0
- package/src/routes/docs/stateCheckpointsList.js +26 -0
- package/src/routes/docs/types.js +33 -0
- package/src/routes/endpoints.js +71 -0
- package/src/routes/feedback.js +110 -0
- package/src/routes/feedback.test.mjs +204 -0
- package/src/routes/jitPage.js +86 -0
- package/src/routes/mcp.js +34 -0
- package/{pages/api → src/routes}/ping.js +4 -2
- package/src/routes/reload.js +79 -0
- package/src/routes/request.js +74 -0
- package/src/routes/request.test.mjs +124 -0
- package/{pages/api → src/routes}/root.js +4 -4
- package/src/routes/usage.js +46 -0
- package/src/routes/websocket.js +33 -0
- package/src/websocket/devWebSocket.js +78 -0
- package/vite.config.js +111 -0
- package/.eslintrc.yaml +0 -1
- package/lib/client/App.js +0 -80
- package/lib/client/Reload.js +0 -60
- package/lib/server/apiWrapper.js +0 -109
- package/lib/server/compileCss.js +0 -39
- package/manager/processes/compileCss.mjs +0 -54
- package/manager/processes/nextBuild.mjs +0 -50
- package/next.config.js +0 -25
- package/pages/_document.js +0 -109
- package/pages/api/agent/[...path].js +0 -86
- package/pages/api/auth/[...nextauth].js +0 -48
- package/pages/api/client-error.js +0 -45
- package/pages/api/js/[env].js +0 -42
- package/pages/api/page/[...pageId].js +0 -74
- package/pages/api/reload.js +0 -53
- package/pages/api/request/[...path].js +0 -41
- /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
- /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
- /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
- /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
- /package/lib/client/{RestartingPage.js → RestartingPage.jsx} +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
// Dev-only agent tooling: an in-memory registry of recorded request
|
|
20
|
+
// responses, keyed by (pageId, requestId). Populated by lib/docs/loadState.js
|
|
21
|
+
// when a state checkpoint is loaded, and consulted by src/routes/request.js
|
|
22
|
+
// so that once a checkpoint is loaded — for a headless verification page, a
|
|
23
|
+
// human tester's browser tab, or a future e2e test driving the dev server
|
|
24
|
+
// directly — its recorded requests replay instead of hitting the real
|
|
25
|
+
// connection. Module-level state is fine: a dev server runs a single process
|
|
26
|
+
// per app.
|
|
27
|
+
const mocks = new Map();
|
|
28
|
+
|
|
29
|
+
function buildKey({ pageId, requestId }) {
|
|
30
|
+
return `${pageId}::${requestId}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// `mocks` here is the `requests` shape read.js/writeCheckpoint deal in:
|
|
34
|
+
// an object keyed by requestId, each value `{ payload, response, error,
|
|
35
|
+
// responseTime }`. Only response/error are replayed; payload/responseTime
|
|
36
|
+
// are recording metadata, not part of the replay contract.
|
|
37
|
+
function loadMocks({ pageId, mocks: entries = {} }) {
|
|
38
|
+
if (type.isNone(pageId) || !type.isString(pageId)) {
|
|
39
|
+
throw new Error(`loadMocks requires a "pageId" string. Received ${JSON.stringify(pageId)}.`);
|
|
40
|
+
}
|
|
41
|
+
Object.entries(entries).forEach(([requestId, entry]) => {
|
|
42
|
+
mocks.set(buildKey({ pageId, requestId }), {
|
|
43
|
+
pageId,
|
|
44
|
+
requestId,
|
|
45
|
+
response: entry?.response ?? null,
|
|
46
|
+
error: entry?.error ?? null,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getMock({ pageId, requestId }) {
|
|
52
|
+
return mocks.get(buildKey({ pageId, requestId }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function clearMocks() {
|
|
56
|
+
mocks.clear();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function listMocks() {
|
|
60
|
+
return Array.from(mocks.values()).map(({ pageId, requestId, response, error }) => ({
|
|
61
|
+
pageId,
|
|
62
|
+
requestId,
|
|
63
|
+
hasResponse: !type.isNone(response),
|
|
64
|
+
hasError: !type.isNone(error),
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { clearMocks, getMock, listMocks, loadMocks };
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
|
|
20
|
+
import { jest } from '@jest/globals';
|
|
21
|
+
|
|
22
|
+
import setupTestFixtures from './setupTestFixtures.mjs';
|
|
23
|
+
|
|
24
|
+
// The docs service reads build artifacts and node_modules from process.cwd()
|
|
25
|
+
// (the running server directory) — point it at the fixture server before the
|
|
26
|
+
// modules are imported, since resolvePluginDir captures cwd at import time.
|
|
27
|
+
const fixtureDir = setupTestFixtures();
|
|
28
|
+
process.chdir(fixtureDir);
|
|
29
|
+
|
|
30
|
+
// runRequest.js calls callRequest (@lowdefy/api) and createLowdefyContext
|
|
31
|
+
// (../server/createLowdefyContext.js, which statically imports the running
|
|
32
|
+
// server's own build/plugins/*.js — not the app fixture built above, and not
|
|
33
|
+
// present in this checkout) — mock both so runRequest can be unit tested
|
|
34
|
+
// without a real server build.
|
|
35
|
+
const mockCallRequest = jest.fn();
|
|
36
|
+
jest.unstable_mockModule('@lowdefy/api', () => ({
|
|
37
|
+
callRequest: mockCallRequest,
|
|
38
|
+
}));
|
|
39
|
+
const mockCreateLowdefyContext = jest.fn(async () => ({
|
|
40
|
+
logger: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
41
|
+
}));
|
|
42
|
+
jest.unstable_mockModule('../server/createLowdefyContext.js', () => ({
|
|
43
|
+
default: mockCreateLowdefyContext,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// findConfig force-builds a page when pageId is passed — the fixture pages
|
|
47
|
+
// only exist as pre-written build artifacts, so stub the JIT builder out.
|
|
48
|
+
const mockBuildPageIfNeeded = jest.fn(async () => true);
|
|
49
|
+
jest.unstable_mockModule('../server/jitPageBuilder.js', () => ({
|
|
50
|
+
default: mockBuildPageIfNeeded,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
const { default: listTypes } = await import('./listTypes.js');
|
|
54
|
+
const { default: listPlugins } = await import('./listPlugins.js');
|
|
55
|
+
const { default: getSchema } = await import('./getSchema.js');
|
|
56
|
+
const { default: getExamples } = await import('./getExamples.js');
|
|
57
|
+
const { default: getCoreDoc } = await import('./getCoreDoc.js');
|
|
58
|
+
const { default: getPluginDoc } = await import('./getPluginDoc.js');
|
|
59
|
+
const { default: getOverview } = await import('./getOverview.js');
|
|
60
|
+
const { default: searchDocs } = await import('./searchDocs.js');
|
|
61
|
+
const { default: normalizeTypeKind } = await import('./normalizeTypeKind.js');
|
|
62
|
+
const { default: clientErrorStore } = await import('./clientErrorStore.js');
|
|
63
|
+
const { default: getBuildStatus } = await import('./getBuildStatus.js');
|
|
64
|
+
const { default: getPageConfig } = await import('./getPageConfig.js');
|
|
65
|
+
const { default: readPageArtifact } = await import('./readPageArtifact.js');
|
|
66
|
+
const { default: findConfig } = await import('./findConfig.js');
|
|
67
|
+
const { default: runRequest } = await import('./runRequest.js');
|
|
68
|
+
const { default: getAppMap } = await import('./getAppMap.js');
|
|
69
|
+
|
|
70
|
+
test('listTypes returns all available blocks with used flag and category', () => {
|
|
71
|
+
const blocks = listTypes({ kind: 'blocks' });
|
|
72
|
+
expect(blocks.map((block) => block.type)).toEqual(['Button', 'TestBlock']);
|
|
73
|
+
const button = blocks.find((block) => block.type === 'Button');
|
|
74
|
+
expect(button.used).toBe(true);
|
|
75
|
+
expect(button.category).toEqual('display');
|
|
76
|
+
const testBlock = blocks.find((block) => block.type === 'TestBlock');
|
|
77
|
+
expect(testBlock.used).toBe(false);
|
|
78
|
+
expect(testBlock.package).toEqual('test-plugin');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('listTypes merges client and server operators with environments', () => {
|
|
82
|
+
const operators = listTypes({ kind: 'operators' });
|
|
83
|
+
const get = operators.find((operator) => operator.type === '_get');
|
|
84
|
+
expect(get.environments).toEqual(['client', 'server']);
|
|
85
|
+
expect(get.used).toBe(true);
|
|
86
|
+
const secret = operators.find((operator) => operator.type === '_secret');
|
|
87
|
+
expect(secret.environments).toEqual(['server']);
|
|
88
|
+
expect(secret.used).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('listTypes accepts singular kind names', () => {
|
|
92
|
+
expect(listTypes({ kind: 'block' }).length).toEqual(2);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('listTypes throws on unknown kind', () => {
|
|
96
|
+
expect(() => listTypes({ kind: 'gadgets' })).toThrow('Unknown type kind');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('listPlugins lists packages with their types and custom flag', () => {
|
|
100
|
+
const plugins = listPlugins();
|
|
101
|
+
const testPlugin = plugins.find((plugin) => plugin.package === 'test-plugin');
|
|
102
|
+
expect(testPlugin.custom).toBe(true);
|
|
103
|
+
expect(testPlugin.installed).toBe(true);
|
|
104
|
+
expect(testPlugin.types.blocks).toEqual(['TestBlock']);
|
|
105
|
+
const antd = plugins.find((plugin) => plugin.package === '@lowdefy/blocks-antd');
|
|
106
|
+
expect(antd.custom).toBe(false);
|
|
107
|
+
expect(antd.types.blocks).toEqual(['Button']);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('getSchema returns block schema with meta', () => {
|
|
111
|
+
const result = getSchema({ kind: 'blocks', type: 'Button' });
|
|
112
|
+
expect(result.schema.properties.title).toBeDefined();
|
|
113
|
+
expect(result.meta.category).toEqual('display');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('getSchema returns connection schema with request list', () => {
|
|
117
|
+
const result = getSchema({ kind: 'connections', type: 'AxiosHttp' });
|
|
118
|
+
expect(result.schema).toBeDefined();
|
|
119
|
+
expect(result.requests).toEqual(['AxiosHttp']);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('getSchema returns request schema with meta', () => {
|
|
123
|
+
const result = getSchema({ kind: 'requests', type: 'AxiosHttp' });
|
|
124
|
+
expect(result.schema).toBeDefined();
|
|
125
|
+
expect(result.meta).toEqual({ checkRead: false, checkWrite: false });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('getSchema returns null for unknown type', () => {
|
|
129
|
+
expect(getSchema({ kind: 'blocks', type: 'Nope' })).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('getSchema throws for kinds without schemas', () => {
|
|
133
|
+
expect(() => getSchema({ kind: 'websockets', type: 'X' })).toThrow('No schemas available');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('getExamples reads example yaml from plugin dist by convention', () => {
|
|
137
|
+
const result = getExamples({ type: 'TestBlock' });
|
|
138
|
+
expect(result.package).toEqual('test-plugin');
|
|
139
|
+
expect(result.files['gallery.yaml']).toContain('type: TestBlock');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('getExamples returns null for unknown block type', () => {
|
|
143
|
+
expect(getExamples({ type: 'Nope' })).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('getPluginDoc returns README markdown for installed plugin', () => {
|
|
147
|
+
const result = getPluginDoc({ packageName: 'test-plugin' });
|
|
148
|
+
expect(result.markdown).toContain('# test-plugin');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('getPluginDoc returns null for packages without docs', () => {
|
|
152
|
+
expect(getPluginDoc({ packageName: 'no-such-package' })).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('getCoreDoc returns markdown by slug from docs-content', () => {
|
|
156
|
+
const doc = getCoreDoc({ slug: 'operators/_get' });
|
|
157
|
+
expect(doc.title).toEqual('_get');
|
|
158
|
+
expect(doc.markdown).toContain('# _get');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('getCoreDoc resolves a doc by kind and type name', () => {
|
|
162
|
+
const doc = getCoreDoc({ kind: 'operator', type: '_get' });
|
|
163
|
+
expect(doc.slug).toEqual('operators/_get');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('getCoreDoc returns null for unknown slug', () => {
|
|
167
|
+
expect(getCoreDoc({ slug: 'nope/nope' })).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('searchDocs returns matching docs with snippets', () => {
|
|
171
|
+
const results = searchDocs({ query: 'SetState' });
|
|
172
|
+
expect(results.length).toBeGreaterThan(0);
|
|
173
|
+
expect(results[0].snippet).toBeDefined();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('searchDocs throws on empty query', () => {
|
|
177
|
+
expect(() => searchDocs({ query: '' })).toThrow('requires a "query" string');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('getOverview includes counts and route guidance', () => {
|
|
181
|
+
const overview = getOverview();
|
|
182
|
+
expect(overview).toContain('2 block types');
|
|
183
|
+
expect(overview).toContain('/lowdefy-docs/schema/{kind}/{type}');
|
|
184
|
+
expect(overview).toContain('lowdefy_list_types');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('normalizeTypeKind maps singular and plural, rejects unknown', () => {
|
|
188
|
+
expect(normalizeTypeKind({ kind: 'block' })).toEqual('blocks');
|
|
189
|
+
expect(normalizeTypeKind({ kind: 'REQUESTS' })).toEqual('requests');
|
|
190
|
+
expect(normalizeTypeKind({ kind: 'nope' })).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('clientErrorStore caps at 50 entries, evicting the oldest first', () => {
|
|
194
|
+
for (let i = 0; i < 60; i += 1) {
|
|
195
|
+
clientErrorStore.push({ index: i });
|
|
196
|
+
}
|
|
197
|
+
const entries = clientErrorStore.list();
|
|
198
|
+
expect(entries.length).toEqual(50);
|
|
199
|
+
expect(entries[0].index).toEqual(10);
|
|
200
|
+
expect(entries[49].index).toEqual(59);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('getBuildStatus returns the build artifact plus reported client errors', () => {
|
|
204
|
+
const result = getBuildStatus();
|
|
205
|
+
expect(result.build.status).toEqual('ok');
|
|
206
|
+
expect(result.clientErrors.length).toEqual(50);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('getBuildStatus reports unknown status when buildStatus.json is missing', () => {
|
|
210
|
+
fs.rmSync(path.join(fixtureDir, 'build', 'buildStatus.json'));
|
|
211
|
+
const result = getBuildStatus();
|
|
212
|
+
expect(result.build.status).toEqual('unknown');
|
|
213
|
+
expect(result.build.message).toContain('No build status yet');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('readPageArtifact reads a built page json artifact', () => {
|
|
217
|
+
const result = readPageArtifact({ pageId: 'home' });
|
|
218
|
+
expect(result.pageId).toEqual('home');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('readPageArtifact returns null when no artifact exists for the pageId', () => {
|
|
222
|
+
expect(readPageArtifact({ pageId: 'no-such-page' })).toBeNull();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('getPageConfig returns null for a pageId not in the page registry', async () => {
|
|
226
|
+
const result = await getPageConfig({ pageId: 'no-such-page' });
|
|
227
|
+
expect(result).toBeNull();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('findConfig resolves a known pageId to its source file', async () => {
|
|
231
|
+
const result = await findConfig({ id: 'home' });
|
|
232
|
+
expect(result.kind).toEqual('page');
|
|
233
|
+
expect(result.pageId).toEqual('home');
|
|
234
|
+
expect(result.file).toEqual('pages/home.yaml');
|
|
235
|
+
// A page's root block renders with blockId === pageId — open-in-editor and
|
|
236
|
+
// feedback enrichment read matches[0].location.source from this result.
|
|
237
|
+
expect(result.matches.length).toEqual(1);
|
|
238
|
+
expect(result.matches[0].location.source).toContain('pages/home.yaml');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('findConfig scans keyMap for a matching id when no pageId is given', async () => {
|
|
242
|
+
// my_button exists on both built pages — an unscoped scan returns both.
|
|
243
|
+
const result = await findConfig({ id: 'my_button' });
|
|
244
|
+
expect(result.matches.length).toEqual(2);
|
|
245
|
+
expect(result.matches[0].keyPath).toEqual('root.blocks[2:my_button:Button]');
|
|
246
|
+
expect(result.matches[0].location.source).toContain('pages/home.yaml:5');
|
|
247
|
+
expect(result.matches[1].keyPath).toEqual('root.blocks[0:my_button:Button]');
|
|
248
|
+
expect(result.note).toContain('Pass ?pageId=');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('findConfig with pageId only returns matches on that page', async () => {
|
|
252
|
+
// Both pages hold a my_button whose JIT key paths are indistinguishable
|
|
253
|
+
// (`root.blocks[N:my_button:Button]`) — scoping must resolve through the
|
|
254
|
+
// ~k_parent chain to the page's own subtree root.
|
|
255
|
+
const result = await findConfig({ id: 'my_button', pageId: 'other' });
|
|
256
|
+
expect(result.matches.length).toEqual(1);
|
|
257
|
+
expect(result.matches[0].keyPath).toEqual('root.blocks[0:my_button:Button]');
|
|
258
|
+
expect(result.matches[0].location.source).toContain('pages/other.yaml:4');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('findConfig with pageId resolves the same id to each page respectively', async () => {
|
|
262
|
+
const result = await findConfig({ id: 'my_button', pageId: 'home' });
|
|
263
|
+
expect(result.matches.length).toEqual(1);
|
|
264
|
+
expect(result.matches[0].keyPath).toEqual('root.blocks[2:my_button:Button]');
|
|
265
|
+
expect(result.matches[0].location.source).toContain('pages/home.yaml:5');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('findConfig scopes skeleton-built pages via the page key segment', async () => {
|
|
269
|
+
// Pages built during the skeleton build (e.g. the default 404) chain to the
|
|
270
|
+
// shared config root — the page segment in the key path identifies them.
|
|
271
|
+
const result = await findConfig({ id: 'legal_button', pageId: 'legal' });
|
|
272
|
+
expect(result.matches.length).toEqual(1);
|
|
273
|
+
expect(result.matches[0].keyPath).toEqual('root.pages[2:legal].blocks[0:legal_button:Button]');
|
|
274
|
+
expect(result.matches[0].location.source).toContain('pages/legal.yaml:3');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('findConfig with a skeleton-built pageId does not leak other pages ids', async () => {
|
|
278
|
+
const result = await findConfig({ id: 'my_button', pageId: 'legal' });
|
|
279
|
+
expect(result.matches).toEqual([]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('findConfig with pageId does not match ids that only exist on other pages', async () => {
|
|
283
|
+
// my_list.$.item_title only exists on home — scoped to "other" it must not
|
|
284
|
+
// resolve, instead of falling back to the wrong page.
|
|
285
|
+
const result = await findConfig({ id: 'my_list.0.item_title', pageId: 'other' });
|
|
286
|
+
expect(result.matches).toEqual([]);
|
|
287
|
+
expect(result.note).toContain('No config found with id "my_list.0.item_title" on page "other"');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('findConfig resolves a runtime list item id to its $ placeholder config id', async () => {
|
|
291
|
+
// A block inside a list renders with array indices applied to its id —
|
|
292
|
+
// the config (and keyMap) hold the `$` form.
|
|
293
|
+
const result = await findConfig({ id: 'my_list.0.item_title' });
|
|
294
|
+
expect(result.matches.length).toEqual(1);
|
|
295
|
+
expect(result.matches[0].keyPath).toContain('[0:my_list.$.item_title:Title]');
|
|
296
|
+
expect(result.matches[0].location.source).toContain('pages/home.yaml:9');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('findConfig resolves a runtime list item id within a pageId scope', async () => {
|
|
300
|
+
const result = await findConfig({ id: 'my_list.0.item_title', pageId: 'home' });
|
|
301
|
+
expect(result.matches.length).toEqual(1);
|
|
302
|
+
expect(result.matches[0].keyPath).toContain('[0:my_list.$.item_title:Title]');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('findConfig de-indexes any list index, not just the first item', async () => {
|
|
306
|
+
const result = await findConfig({ id: 'my_list.17.item_title' });
|
|
307
|
+
expect(result.matches.length).toEqual(1);
|
|
308
|
+
expect(result.matches[0].keyPath).toContain('my_list.$.item_title');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('findConfig prefers an exact id match over de-indexing', async () => {
|
|
312
|
+
// my_button contains no indices — exact scan must resolve it without the
|
|
313
|
+
// de-index retry changing anything.
|
|
314
|
+
const result = await findConfig({ id: 'my_button' });
|
|
315
|
+
expect(result.matches[0].keyPath).toEqual('root.blocks[2:my_button:Button]');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('findConfig returns empty matches with a note when nothing matches', async () => {
|
|
319
|
+
const result = await findConfig({ id: 'no-such-id' });
|
|
320
|
+
expect(result.matches).toEqual([]);
|
|
321
|
+
expect(result.note).toBeDefined();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('findConfig throws when id is missing', async () => {
|
|
325
|
+
await expect(findConfig({ id: undefined })).rejects.toThrow('requires an "id" string');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('runRequest throws when pageId is missing', async () => {
|
|
329
|
+
await expect(runRequest({ requestId: 'req-read' })).rejects.toThrow('requires a "pageId"');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('runRequest throws when requestId is missing', async () => {
|
|
333
|
+
await expect(runRequest({ pageId: 'home' })).rejects.toThrow('requires a "requestId"');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('runRequest refuses when the requestId does not exist on the page', async () => {
|
|
337
|
+
const result = await runRequest({
|
|
338
|
+
pageId: 'home',
|
|
339
|
+
requestId: 'no-such-request',
|
|
340
|
+
honoContext: {},
|
|
341
|
+
});
|
|
342
|
+
expect(result.refused).toBe(true);
|
|
343
|
+
expect(result.reason).toContain('was not found');
|
|
344
|
+
expect(mockCallRequest).not.toHaveBeenCalled();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('runRequest allows a request type with checkWrite: false without the write opt-in', async () => {
|
|
348
|
+
mockCallRequest.mockResolvedValueOnce({
|
|
349
|
+
id: 'req-read',
|
|
350
|
+
success: true,
|
|
351
|
+
type: 'ReadOnlyRequest',
|
|
352
|
+
response: { ok: true },
|
|
353
|
+
});
|
|
354
|
+
const result = await runRequest({ pageId: 'home', requestId: 'req-read', honoContext: {} });
|
|
355
|
+
expect(result.refused).toBe(false);
|
|
356
|
+
expect(result.response).toEqual({ ok: true });
|
|
357
|
+
expect(mockCallRequest).toHaveBeenCalledTimes(1);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('runRequest refuses a checkWrite: true request type when the opt-in is not set', async () => {
|
|
361
|
+
const result = await runRequest({ pageId: 'home', requestId: 'req-write', honoContext: {} });
|
|
362
|
+
expect(result.refused).toBe(true);
|
|
363
|
+
expect(result.reason).toContain('checkWrite: true');
|
|
364
|
+
expect(result.howToEnable).toContain('cli.agentTools.allowWriteRequests');
|
|
365
|
+
expect(mockCallRequest).not.toHaveBeenCalled();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test('runRequest refuses a request type with no declared meta', async () => {
|
|
369
|
+
const result = await runRequest({ pageId: 'home', requestId: 'req-unknown', honoContext: {} });
|
|
370
|
+
expect(result.refused).toBe(true);
|
|
371
|
+
expect(result.reason).toContain('no declared read/write meta');
|
|
372
|
+
expect(mockCallRequest).not.toHaveBeenCalled();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('runRequest allows a checkWrite: true request type once cli.agentTools.allowWriteRequests is set', async () => {
|
|
376
|
+
fs.writeFileSync(
|
|
377
|
+
path.join(fixtureDir, 'lowdefy.yaml'),
|
|
378
|
+
'lowdefy: 5.0.0\ncli:\n agentTools:\n allowWriteRequests: true\n'
|
|
379
|
+
);
|
|
380
|
+
mockCallRequest.mockResolvedValueOnce({
|
|
381
|
+
id: 'req-write',
|
|
382
|
+
success: true,
|
|
383
|
+
type: 'WriteRequest',
|
|
384
|
+
response: { updated: true },
|
|
385
|
+
});
|
|
386
|
+
try {
|
|
387
|
+
const result = await runRequest({ pageId: 'home', requestId: 'req-write', honoContext: {} });
|
|
388
|
+
expect(result.refused).toBe(false);
|
|
389
|
+
expect(result.response).toEqual({ updated: true });
|
|
390
|
+
expect(mockCallRequest).toHaveBeenCalledTimes(1);
|
|
391
|
+
} finally {
|
|
392
|
+
fs.writeFileSync(path.join(fixtureDir, 'lowdefy.yaml'), 'lowdefy: 5.0.0\n');
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('runRequest returns a structured error instead of throwing when callRequest fails', async () => {
|
|
397
|
+
mockCallRequest.mockRejectedValueOnce(new Error('boom'));
|
|
398
|
+
const result = await runRequest({ pageId: 'home', requestId: 'req-read', honoContext: {} });
|
|
399
|
+
expect(result.refused).toBe(false);
|
|
400
|
+
expect(result.error).toEqual({ name: 'Error', message: 'boom' });
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('runRequest truncates responses larger than the size cap', async () => {
|
|
404
|
+
mockCallRequest.mockResolvedValueOnce({
|
|
405
|
+
id: 'req-read',
|
|
406
|
+
success: true,
|
|
407
|
+
type: 'ReadOnlyRequest',
|
|
408
|
+
response: 'x'.repeat(200_000),
|
|
409
|
+
});
|
|
410
|
+
const result = await runRequest({ pageId: 'home', requestId: 'req-read', honoContext: {} });
|
|
411
|
+
expect(result.truncated).toBe(true);
|
|
412
|
+
expect(result.note).toContain('truncated');
|
|
413
|
+
expect(result.response.length).toEqual(100_000);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('getAppMap includes built page detail and a note for unbuilt pages', () => {
|
|
417
|
+
const map = getAppMap();
|
|
418
|
+
|
|
419
|
+
const home = map.pages.find((page) => page.pageId === 'home');
|
|
420
|
+
expect(home.built).toBe(true);
|
|
421
|
+
expect(home.file).toEqual('pages/home.yaml');
|
|
422
|
+
expect(home.blockCount).toEqual(3);
|
|
423
|
+
expect(home.blockTypes).toEqual(['Button', 'Card']);
|
|
424
|
+
expect(home.requests).toEqual([
|
|
425
|
+
{ id: 'req-read', type: 'ReadOnlyRequest' },
|
|
426
|
+
{ id: 'req-write', type: 'WriteRequest' },
|
|
427
|
+
{ id: 'req-unknown', type: 'UnknownMetaRequest' },
|
|
428
|
+
]);
|
|
429
|
+
|
|
430
|
+
const unbuilt = map.pages.find((page) => page.pageId === 'unbuilt');
|
|
431
|
+
expect(unbuilt.built).toBe(false);
|
|
432
|
+
expect(unbuilt.blockCount).toBeUndefined();
|
|
433
|
+
expect(map.note).toContain('1 page(s) have not been built');
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('getAppMap includes connections, endpoints, agents, menus and websockets', () => {
|
|
437
|
+
const map = getAppMap();
|
|
438
|
+
expect(map.connections).toEqual([{ id: 'axios', type: 'AxiosHttp' }]);
|
|
439
|
+
expect(map.endpoints).toEqual([{ id: 'resolve_greeting', type: 'InternalApi' }]);
|
|
440
|
+
expect(map.agents).toEqual([{ id: 'assistant', type: 'OpenAiAgent' }]);
|
|
441
|
+
expect(map.websockets).toEqual([]);
|
|
442
|
+
expect(map.menus).toEqual([
|
|
443
|
+
{
|
|
444
|
+
menuId: 'default',
|
|
445
|
+
links: [{ menuItemId: 'home', type: 'MenuLink', pageId: 'home', title: 'Home' }],
|
|
446
|
+
},
|
|
447
|
+
]);
|
|
448
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import findConfig from './findConfig.js';
|
|
20
|
+
|
|
21
|
+
// Resolves each annotation's target.blockId to a config source location
|
|
22
|
+
// (file:line) via findConfig, which force-builds the batch's page — so an
|
|
23
|
+
// annotation drawn on a block the agent hasn't looked at yet still resolves.
|
|
24
|
+
// Runs before the batch is queued (src/routes/feedback.js) so
|
|
25
|
+
// formatFeedback.js can stay a pure string formatter with no build-artifact
|
|
26
|
+
// access of its own.
|
|
27
|
+
async function enrichFeedback({ batch }) {
|
|
28
|
+
const annotations = await Promise.all(
|
|
29
|
+
(batch.annotations ?? []).map((annotation) => enrichAnnotation({ annotation, batch }))
|
|
30
|
+
);
|
|
31
|
+
return { ...batch, annotations };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function enrichAnnotation({ annotation, batch }) {
|
|
35
|
+
const blockId = annotation.target?.blockId;
|
|
36
|
+
if (type.isNone(blockId)) {
|
|
37
|
+
return annotation;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
// Dynamically generated blocks (Dynamic endpoints, list items) have ids
|
|
41
|
+
// that exist only at runtime — walk up the ancestor chain until an id
|
|
42
|
+
// resolves in config, so the annotation still points at the yaml that
|
|
43
|
+
// produces the block.
|
|
44
|
+
const candidates = [blockId, ...(annotation.target?.ancestorBlockIds ?? []).slice(1)];
|
|
45
|
+
for (const candidateId of candidates) {
|
|
46
|
+
const result = await findConfig({ id: candidateId, pageId: batch.pageId });
|
|
47
|
+
const location = result.matches?.[0]?.location;
|
|
48
|
+
if (location) {
|
|
49
|
+
if (candidateId !== blockId) {
|
|
50
|
+
return {
|
|
51
|
+
...annotation,
|
|
52
|
+
location: { ...location, resolvedVia: candidateId },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { ...annotation, location };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
...annotation,
|
|
60
|
+
location: {
|
|
61
|
+
note: `Block is generated at runtime — no configured ancestor found on page "${batch.pageId}".`,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Never drop feedback because location resolution failed — the
|
|
66
|
+
// developer's annotation is still useful without a file:line pointer.
|
|
67
|
+
return { ...annotation, location: { note: `Failed to resolve location: ${error.message}` } };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default enrichFeedback;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import evalOperatorInTab from './evalOperatorInTab.js';
|
|
18
|
+
import evalOperatorHeadless from './evalOperatorHeadless.js';
|
|
19
|
+
import resolveSource from './resolveSource.js';
|
|
20
|
+
|
|
21
|
+
// resolveSource picks the tab or headless — including why `user` is
|
|
22
|
+
// headless-only. Falls back to headless whenever the tab path isn't usable
|
|
23
|
+
// (no tab connected, or `source: 'tab'` was requested but it errored) so
|
|
24
|
+
// agents always get an answer.
|
|
25
|
+
async function evalOperator({ origin, pageId, expression, source, user }) {
|
|
26
|
+
const { tryTab, error, invalidInput } = resolveSource({
|
|
27
|
+
name: 'evalOperator',
|
|
28
|
+
pageId,
|
|
29
|
+
source,
|
|
30
|
+
user,
|
|
31
|
+
});
|
|
32
|
+
if (error) {
|
|
33
|
+
return { error, invalidInput };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (tryTab) {
|
|
37
|
+
const result = await evalOperatorInTab({ pageId, expression });
|
|
38
|
+
if (!result?.error) {
|
|
39
|
+
return { ...result, source: 'tab' };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = await evalOperatorHeadless({ origin, pageId, expression, user });
|
|
44
|
+
return { ...result, source: 'headless' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default evalOperator;
|