@lowdefy/server-dev 0.0.0-experimental-20260720072521 → 0.0.0-experimental-20260720135014
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/.eslintrc.yaml +1 -0
- package/lib/build/app.js +1 -8
- package/lib/build/appMeta.js +1 -8
- package/lib/build/auth.js +1 -8
- package/lib/build/config.js +1 -8
- package/lib/build/i18n.js +1 -8
- package/lib/build/logger.js +1 -8
- package/lib/build/theme.js +1 -8
- package/lib/client/App.js +80 -0
- package/{client/Page.jsx → lib/client/Page.js} +5 -5
- package/{client/Reload.jsx → lib/client/Reload.js} +7 -5
- package/lib/client/auth/{Auth.jsx → Auth.js} +5 -12
- package/lib/client/auth/AuthConfigured.js +61 -0
- package/lib/client/auth/AuthNotConfigured.js +2 -22
- package/lib/client/setPageId.js +5 -6
- package/lib/client/utils/useMutateCache.js +1 -14
- package/lib/client/utils/usePageConfig.js +11 -31
- package/lib/server/apiWrapper.js +116 -0
- package/lib/server/auth/{getStrategies.js → getAuthOptions.js} +9 -14
- package/lib/server/auth/{getMockUser.js → getMockSession.js} +19 -10
- package/{src/routes/docs/checkpointsRevert.js → lib/server/auth/getServerSession.js} +13 -11
- package/lib/server/compileCss.js +39 -0
- package/lib/server/jitPageBuilder.js +11 -36
- package/lib/server/log/logRequest.js +3 -1
- package/manager/getContext.mjs +7 -3
- package/manager/processes/compileCss.mjs +54 -0
- package/manager/processes/initialBuild.mjs +2 -0
- package/manager/processes/installPlugins.mjs +11 -19
- package/manager/processes/lowdefyBuild.mjs +7 -29
- package/manager/processes/nextBuild.mjs +50 -0
- package/manager/processes/readDotEnv.mjs +1 -13
- package/manager/processes/shutdownServer.mjs +6 -6
- package/manager/processes/startServer.mjs +7 -8
- package/manager/processes/startWatchers.mjs +2 -2
- package/manager/run.mjs +0 -41
- package/manager/utils/createCustomPluginMessagesMap.mjs +1 -8
- package/manager/utils/createCustomPluginTypesMap.mjs +2 -3
- package/manager/utils/{getViteBin.mjs → getNextBin.mjs} +10 -7
- package/manager/utils/updatePageTailwindCss.mjs +1 -7
- package/manager/watchers/lowdefyBuildWatcher.mjs +1 -0
- package/manager/watchers/moduleBuildWatcher.mjs +1 -0
- package/manager/watchers/{serverArtifactWatcher.mjs → nextBuildWatcher.mjs} +22 -18
- package/next.config.js +25 -0
- package/package.json +47 -63
- package/package.original.json +55 -66
- package/{src/routes/devTools.js → pages/404.js} +2 -4
- package/{src/routes/docs/appMap.js → pages/[[...pageId]].js} +2 -6
- package/{client/App.jsx → pages/_app.js} +33 -21
- package/pages/_document.js +107 -0
- package/pages/api/agent/[...path].js +86 -0
- package/pages/api/auth/[...nextauth].js +48 -0
- package/pages/api/client-error.js +45 -0
- package/{src/routes/docs/buildStatus.js → pages/api/dev-tools.js} +4 -5
- package/{lib/docs/clientErrorStore.js → pages/api/endpoints/[...endpointId].js} +11 -15
- package/{src/lib/serveBuildJs.js → pages/api/icons/dynamic.js} +12 -8
- package/{src/routes/jsEnv.js → pages/api/js/[env].js} +20 -8
- package/pages/api/page/[...pageId].js +74 -0
- package/{src/routes → pages/api}/ping.js +2 -4
- package/pages/api/reload.js +53 -0
- package/pages/api/request/[...path].js +41 -0
- package/{src/routes → pages/api}/root.js +4 -4
- package/client/Inspector.jsx +0 -206
- package/client/Routing.jsx +0 -106
- package/client/feedback/FeedbackMount.jsx +0 -83
- package/client/feedback/FeedbackOverlay.jsx +0 -901
- package/client/feedback/captureTabScreenshot.js +0 -84
- package/client/feedback/copyToClipboard.js +0 -45
- package/client/feedback/drawAnnotationsSvg.js +0 -119
- package/client/feedback/elementInspect.js +0 -99
- package/client/feedback/feedbackStyles.js +0 -301
- package/client/feedback/pinResponsiveImages.js +0 -59
- package/client/feedback/sendFeedback.js +0 -39
- package/client/feedback/useFeedbackToggle.js +0 -70
- package/client/main.jsx +0 -42
- package/client/openInEditor/OpenInEditorListener.jsx +0 -261
- package/client/openInEditor/findBlockLocation.js +0 -51
- package/lib/client/auth/AuthConfigured.jsx +0 -155
- package/lib/docs/captureAnnotatedScreenshot.js +0 -88
- package/lib/docs/checkpointPaths.js +0 -91
- package/lib/docs/checkpointStore.js +0 -234
- package/lib/docs/checkpointToMocks.js +0 -65
- package/lib/docs/configCheckpoints.test.mjs +0 -156
- package/lib/docs/createConfigCheckpoint.js +0 -92
- package/lib/docs/createDocsMcpServer.js +0 -506
- package/lib/docs/createDocsMcpServer.test.mjs +0 -123
- package/lib/docs/devMockRegistry.js +0 -68
- package/lib/docs/docs.test.mjs +0 -448
- package/lib/docs/enrichFeedback.js +0 -71
- package/lib/docs/evalOperator.js +0 -39
- package/lib/docs/evalOperator.test.mjs +0 -112
- package/lib/docs/evalOperatorHeadless.js +0 -87
- package/lib/docs/evalOperatorHeadless.test.mjs +0 -72
- package/lib/docs/evalOperatorInTab.js +0 -48
- package/lib/docs/findConfig.js +0 -260
- package/lib/docs/formatFeedback.js +0 -121
- package/lib/docs/formatFeedback.test.mjs +0 -204
- package/lib/docs/getAppMap.js +0 -180
- package/lib/docs/getBrowser.js +0 -115
- package/lib/docs/getBuildStatus.js +0 -37
- package/lib/docs/getCoreDoc.js +0 -63
- package/lib/docs/getDocsManifest.js +0 -40
- package/lib/docs/getExamples.js +0 -58
- package/lib/docs/getOverview.js +0 -115
- package/lib/docs/getPageConfig.js +0 -53
- package/lib/docs/getPluginDoc.js +0 -55
- package/lib/docs/getSchema.js +0 -65
- package/lib/docs/inspectState.js +0 -41
- package/lib/docs/inspectState.test.mjs +0 -91
- package/lib/docs/inspectStateFromTab.js +0 -40
- package/lib/docs/inspectStateHeadless.js +0 -84
- package/lib/docs/inspectStateHeadless.test.mjs +0 -60
- package/lib/docs/isWriteRequestsAllowed.js +0 -50
- package/lib/docs/listConfigCheckpoints.js +0 -55
- package/lib/docs/listPlugins.js +0 -80
- package/lib/docs/listTypes.js +0 -105
- package/lib/docs/loadState.js +0 -139
- package/lib/docs/mapPageBuildErrors.js +0 -28
- package/lib/docs/normalizeTypeKind.js +0 -40
- package/lib/docs/readBuildArtifact.js +0 -35
- package/lib/docs/readPageArtifact.js +0 -26
- package/lib/docs/resolvePluginDir.js +0 -49
- package/lib/docs/revertConfigCheckpoint.js +0 -73
- package/lib/docs/runRequest.js +0 -141
- package/lib/docs/saveAnnotatedScreenshot.js +0 -43
- package/lib/docs/scaffoldPage.js +0 -75
- package/lib/docs/scaffoldPage.test.mjs +0 -97
- package/lib/docs/screenshotPage.js +0 -109
- package/lib/docs/screenshotPage.test.mjs +0 -59
- package/lib/docs/screenshotPageClip.test.mjs +0 -102
- package/lib/docs/searchDocs.js +0 -67
- package/lib/docs/setupTestFixtures.mjs +0 -335
- package/lib/docs/snapshotState.js +0 -59
- package/lib/docs/stateCheckpoints.test.mjs +0 -348
- package/lib/docs/tabAvailable.js +0 -28
- package/lib/docs/tabChannel.js +0 -119
- package/lib/docs/tabChannel.test.mjs +0 -130
- package/lib/server/auth/createSystemContext.js +0 -70
- package/lib/server/auth/getAuth.js +0 -46
- package/lib/server/auth/getHeadlessUser.js +0 -39
- package/lib/server/auth/headlessUser.js +0 -30
- package/lib/server/createLowdefyContext.js +0 -133
- package/lib/server/loadDynamicJsMap.js +0 -47
- package/manager/processes/lowdefyBuild.test.mjs +0 -102
- package/manager/utils/formatNoticeBox.mjs +0 -30
- package/manager/utils/writeBuildStatus.mjs +0 -39
- package/manager/utils/writeBuildStatus.test.mjs +0 -76
- package/postcss.config.cjs +0 -1
- package/src/app.js +0 -188
- package/src/html/renderDevPage.js +0 -86
- package/src/lib/getPathSegments.js +0 -25
- package/src/lib/safeScriptJson.js +0 -38
- package/src/middleware/apiContext.js +0 -32
- package/src/middleware/errorHandler.js +0 -55
- package/src/routes/agent.js +0 -64
- package/src/routes/auth.js +0 -43
- package/src/routes/clientError.js +0 -57
- package/src/routes/cron.js +0 -44
- package/src/routes/detached.js +0 -51
- package/src/routes/devInspect.js +0 -94
- package/src/routes/docs/checkpointsCreate.js +0 -28
- package/src/routes/docs/checkpointsList.js +0 -23
- package/src/routes/docs/content.js +0 -31
- package/src/routes/docs/evalOperator.js +0 -46
- package/src/routes/docs/examples.js +0 -33
- package/src/routes/docs/find.js +0 -30
- package/src/routes/docs/index.js +0 -23
- package/src/routes/docs/inspectState.js +0 -34
- package/src/routes/docs/loadState.js +0 -38
- package/src/routes/docs/mcp.js +0 -30
- package/src/routes/docs/pageConfig.js +0 -36
- package/src/routes/docs/pluginDoc.js +0 -33
- package/src/routes/docs/plugins.js +0 -23
- package/src/routes/docs/runRequest.js +0 -34
- package/src/routes/docs/schema.js +0 -37
- package/src/routes/docs/screenshot.js +0 -66
- package/src/routes/docs/search.js +0 -27
- package/src/routes/docs/snapshotState.js +0 -36
- package/src/routes/docs/stateCheckpointsList.js +0 -26
- package/src/routes/docs/types.js +0 -33
- package/src/routes/endpoints.js +0 -76
- package/src/routes/feedback.js +0 -110
- package/src/routes/feedback.test.mjs +0 -204
- package/src/routes/iconsDynamic.js +0 -25
- package/src/routes/jitPage.js +0 -99
- package/src/routes/reload.js +0 -81
- package/src/routes/request.js +0 -66
- package/src/routes/usage.js +0 -46
- package/src/routes/user.js +0 -27
- package/src/routes/websocket.js +0 -33
- package/src/websocket/devWebSocket.js +0 -78
- package/vite.config.js +0 -111
- /package/lib/client/{BuildErrorPage.jsx → BuildErrorPage.js} +0 -0
- /package/lib/client/{BuildingPage.jsx → BuildingPage.js} +0 -0
- /package/lib/client/{ErrorBar.jsx → ErrorBar.js} +0 -0
- /package/lib/client/{InstallingPluginsPage.jsx → InstallingPluginsPage.js} +0 -0
- /package/lib/client/{RestartingPage.jsx → RestartingPage.js} +0 -0
|
@@ -1,88 +0,0 @@
|
|
|
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 'node:fs';
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
|
|
20
|
-
import drawAnnotationsSvg from '../../client/feedback/drawAnnotationsSvg.js';
|
|
21
|
-
import { getBrowser, openPage } from './getBrowser.js';
|
|
22
|
-
|
|
23
|
-
// Renders the page headless at the batch's recorded viewport/scroll, injects
|
|
24
|
-
// the developer's drawn shapes and element outlines as an SVG layer inside
|
|
25
|
-
// the page, screenshots the composite, and saves it under the config dir's
|
|
26
|
-
// .lowdefy/annotations/ (gitignored). The saved path goes into the formatted
|
|
27
|
-
// feedback text so an agent can read the image. Never throws — returns
|
|
28
|
-
// { path } or { error }.
|
|
29
|
-
async function captureAnnotatedScreenshot({ origin, batch, fileName }) {
|
|
30
|
-
let browser;
|
|
31
|
-
try {
|
|
32
|
-
browser = await getBrowser();
|
|
33
|
-
} catch (error) {
|
|
34
|
-
return { error: `No Chromium available for the annotated screenshot (${error.message}).` };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const viewport = batch.viewport ?? {};
|
|
38
|
-
const width = viewport.width || 1280;
|
|
39
|
-
const height = viewport.height || 800;
|
|
40
|
-
const urlQuery = batch.urlQuery && batch.urlQuery !== '?' ? batch.urlQuery : '';
|
|
41
|
-
|
|
42
|
-
let context;
|
|
43
|
-
try {
|
|
44
|
-
const opened = await openPage({
|
|
45
|
-
browser,
|
|
46
|
-
origin,
|
|
47
|
-
pageId: `${batch.pageId}${urlQuery}`,
|
|
48
|
-
width,
|
|
49
|
-
height,
|
|
50
|
-
});
|
|
51
|
-
context = opened.context;
|
|
52
|
-
const { page } = opened;
|
|
53
|
-
|
|
54
|
-
if (viewport.scrollX || viewport.scrollY) {
|
|
55
|
-
await page.evaluate(
|
|
56
|
-
([x, y]) => window.scrollTo(x, y),
|
|
57
|
-
[viewport.scrollX ?? 0, viewport.scrollY ?? 0]
|
|
58
|
-
);
|
|
59
|
-
await page.waitForTimeout(200);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Shapes and element rects are viewport-relative at the recorded scroll
|
|
63
|
-
// offset — after scrolling to the same offset they overlay 1:1. The
|
|
64
|
-
// compositor is shared with the in-tab capture (drawAnnotationsSvg is
|
|
65
|
-
// self-contained, so Playwright can serialize it into the page).
|
|
66
|
-
await page.evaluate(drawAnnotationsSvg, batch.annotations ?? []);
|
|
67
|
-
await page.waitForTimeout(150);
|
|
68
|
-
|
|
69
|
-
const buffer = await page.screenshot({ type: 'png' });
|
|
70
|
-
|
|
71
|
-
const configDirectory = process.env.LOWDEFY_DIRECTORY_CONFIG || process.cwd();
|
|
72
|
-
const dir = path.join(configDirectory, '.lowdefy', 'annotations');
|
|
73
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
74
|
-
const name =
|
|
75
|
-
fileName ?? `${batch.pageId}-${new Date().toISOString().replace(/[:.]/g, '-')}.png`;
|
|
76
|
-
fs.writeFileSync(path.join(dir, name), buffer);
|
|
77
|
-
|
|
78
|
-
return { path: path.join('.lowdefy', 'annotations', name) };
|
|
79
|
-
} catch (error) {
|
|
80
|
-
return { error: `Failed to capture annotated screenshot: ${error.message}` };
|
|
81
|
-
} finally {
|
|
82
|
-
if (context) {
|
|
83
|
-
await context.close();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export default captureAnnotatedScreenshot;
|
|
@@ -1,91 +0,0 @@
|
|
|
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
|
-
// Shared path/file-listing helpers for createConfigCheckpoint.js,
|
|
21
|
-
// listConfigCheckpoints.js and revertConfigCheckpoint.js.
|
|
22
|
-
|
|
23
|
-
const CONFIG_FILE_EXTENSIONS = ['.yaml', '.yml', '.json', '.md', '.js', '.env'];
|
|
24
|
-
const EXCLUDED_DIRECTORY_NAMES = ['.lowdefy', 'node_modules', '.git', 'checkpoints'];
|
|
25
|
-
const MAX_CHECKPOINTS = 20;
|
|
26
|
-
const MANIFEST_FILE_NAME = 'manifest.json';
|
|
27
|
-
|
|
28
|
-
function getConfigDirectory() {
|
|
29
|
-
return process.env.LOWDEFY_DIRECTORY_CONFIG || process.cwd();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function getCheckpointsRoot() {
|
|
33
|
-
return path.join(getConfigDirectory(), '.lowdefy', 'checkpoints');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// path.extname('.env') is '' (dotfiles have no extension in Node's eyes), so a
|
|
37
|
-
// dotfile named exactly ".env" needs its own basename check.
|
|
38
|
-
function isConfigFile(filePath) {
|
|
39
|
-
if (path.basename(filePath) === '.env') {
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
return CONFIG_FILE_EXTENSIONS.includes(path.extname(filePath));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Recursively lists config-relevant files under `configDirectory`, returned as
|
|
46
|
-
// paths relative to it. Skips directories that should never be snapshotted —
|
|
47
|
-
// build output (.lowdefy, which also holds the checkpoints themselves),
|
|
48
|
-
// dependencies (node_modules) and version control (.git).
|
|
49
|
-
function listConfigFiles({ configDirectory }) {
|
|
50
|
-
const results = [];
|
|
51
|
-
|
|
52
|
-
function walk(currentDir) {
|
|
53
|
-
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
54
|
-
const fullPath = path.join(currentDir, entry.name);
|
|
55
|
-
if (entry.isDirectory()) {
|
|
56
|
-
if (EXCLUDED_DIRECTORY_NAMES.includes(entry.name)) {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
walk(fullPath);
|
|
60
|
-
} else if (entry.isFile() && isConfigFile(fullPath)) {
|
|
61
|
-
results.push(path.relative(configDirectory, fullPath));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (fs.existsSync(configDirectory)) {
|
|
67
|
-
walk(configDirectory);
|
|
68
|
-
}
|
|
69
|
-
return results;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Slugifies a checkpoint label for use in a directory name — lowercase,
|
|
73
|
-
// non-alphanumeric runs collapsed to a single "-", leading/trailing "-"
|
|
74
|
-
// trimmed. Falls back to "checkpoint" when the label slugifies to nothing
|
|
75
|
-
// (e.g. an empty string or a label made only of punctuation).
|
|
76
|
-
function slug(label) {
|
|
77
|
-
const slugified = String(label ?? '')
|
|
78
|
-
.toLowerCase()
|
|
79
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
80
|
-
.replace(/^-+|-+$/g, '');
|
|
81
|
-
return slugified === '' ? 'checkpoint' : slugified;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export {
|
|
85
|
-
getCheckpointsRoot,
|
|
86
|
-
getConfigDirectory,
|
|
87
|
-
listConfigFiles,
|
|
88
|
-
MANIFEST_FILE_NAME,
|
|
89
|
-
MAX_CHECKPOINTS,
|
|
90
|
-
slug,
|
|
91
|
-
};
|
|
@@ -1,234 +0,0 @@
|
|
|
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
|
-
import { serializer, type } from '@lowdefy/helpers';
|
|
20
|
-
|
|
21
|
-
import { getConfigDirectory } from './checkpointPaths.js';
|
|
22
|
-
|
|
23
|
-
// State checkpoints (page state + recorded request/api responses) live under
|
|
24
|
-
// <configDir>/.lowdefy/state-checkpoints/<name>/ — inside .lowdefy because a
|
|
25
|
-
// checkpoint captures the signed-in user's data and live backend responses,
|
|
26
|
-
// which must not sit in a committable spot by default (.lowdefy is gitignored
|
|
27
|
-
// by convention and ignored by the dev file watcher). A sibling of
|
|
28
|
-
// .lowdefy/checkpoints (the config-directory-snapshot checkpoints in
|
|
29
|
-
// checkpointPaths.js/createConfigCheckpoint.js) rather than a subdirectory,
|
|
30
|
-
// because that store prunes its oldest directories beyond a cap. These two
|
|
31
|
-
// "checkpoint" concepts are unrelated: this one is for putting the *running
|
|
32
|
-
// app* into a specific state (agent verification, human testing, e2e test
|
|
33
|
-
// generation), not for reverting config files.
|
|
34
|
-
const NAME_PATTERN = /^[a-z0-9-_]+$/i;
|
|
35
|
-
|
|
36
|
-
function getStateCheckpointsRoot() {
|
|
37
|
-
return path.join(getConfigDirectory(), '.lowdefy', 'state-checkpoints');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function validateName(name) {
|
|
41
|
-
if (type.isNone(name) || !type.isString(name)) {
|
|
42
|
-
throw new Error(`Checkpoint "name" must be a string. Received ${JSON.stringify(name)}.`);
|
|
43
|
-
}
|
|
44
|
-
if (!NAME_PATTERN.test(name)) {
|
|
45
|
-
throw new Error(
|
|
46
|
-
`Checkpoint name "${name}" is invalid — use only letters, numbers, "-" and "_".`
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function writeJsonFile(filePath, value) {
|
|
52
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
53
|
-
fs.writeFileSync(filePath, JSON.stringify(serializer.serialize(value), null, 2));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function readJsonFile(filePath, defaultValue) {
|
|
57
|
-
if (!fs.existsSync(filePath)) {
|
|
58
|
-
return defaultValue;
|
|
59
|
-
}
|
|
60
|
-
return serializer.deserialize(JSON.parse(fs.readFileSync(filePath, 'utf8')));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Requests/api responses arrive as either an array of entries (newest first —
|
|
64
|
-
// the shape of context.requests in both Requests.js and Inspector.jsx's
|
|
65
|
-
// buildSnapshot) or a single entry object. Either way only the newest
|
|
66
|
-
// recording is kept per id.
|
|
67
|
-
function newestEntry(value) {
|
|
68
|
-
return Array.isArray(value) ? value[0] : value;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function writeEntries({ dir, subdir, entries, parts }) {
|
|
72
|
-
Object.entries(entries ?? {}).forEach(([id, value]) => {
|
|
73
|
-
const entry = newestEntry(value);
|
|
74
|
-
if (type.isNone(entry)) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const { payload, response, error, responseTime } = entry;
|
|
78
|
-
writeJsonFile(path.join(dir, subdir, `${id}.json`), { payload, response, error, responseTime });
|
|
79
|
-
parts.push(`${subdir}/${id}.json`);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function readEntries({ dir, subdir }) {
|
|
84
|
-
const entriesDir = path.join(dir, subdir);
|
|
85
|
-
const entries = {};
|
|
86
|
-
if (!fs.existsSync(entriesDir)) {
|
|
87
|
-
return entries;
|
|
88
|
-
}
|
|
89
|
-
fs.readdirSync(entriesDir)
|
|
90
|
-
.filter((fileName) => fileName.endsWith('.json'))
|
|
91
|
-
.forEach((fileName) => {
|
|
92
|
-
const id = fileName.slice(0, -'.json'.length);
|
|
93
|
-
entries[id] = readJsonFile(path.join(entriesDir, fileName), null);
|
|
94
|
-
});
|
|
95
|
-
return entries;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Snapshots a running page's state + recorded request/api responses into a
|
|
99
|
-
// checkpoint folder. Refuses to clobber an existing checkpoint unless
|
|
100
|
-
// `overwrite: true` — when overwriting, the old folder is removed first so a
|
|
101
|
-
// checkpoint written with fewer requests than a previous one doesn't leave
|
|
102
|
-
// stale per-request files behind.
|
|
103
|
-
function writeCheckpoint({ name, snapshot, notes, overwrite }) {
|
|
104
|
-
validateName(name);
|
|
105
|
-
if (type.isNone(snapshot) || !type.isObject(snapshot)) {
|
|
106
|
-
throw new Error(
|
|
107
|
-
`writeCheckpoint requires a "snapshot" object. Received ${JSON.stringify(snapshot)}.`
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const dir = path.join(getStateCheckpointsRoot(), name);
|
|
112
|
-
const exists = fs.existsSync(dir);
|
|
113
|
-
if (exists && overwrite !== true) {
|
|
114
|
-
throw new Error(`Checkpoint "${name}" already exists. Pass overwrite: true to replace it.`);
|
|
115
|
-
}
|
|
116
|
-
if (exists) {
|
|
117
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
118
|
-
}
|
|
119
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
120
|
-
|
|
121
|
-
const parts = [];
|
|
122
|
-
const manifest = {
|
|
123
|
-
name,
|
|
124
|
-
pageId: snapshot.pageId ?? null,
|
|
125
|
-
url: snapshot.url ?? null,
|
|
126
|
-
capturedAt: new Date().toISOString(),
|
|
127
|
-
notes: notes ?? null,
|
|
128
|
-
};
|
|
129
|
-
writeJsonFile(path.join(dir, 'checkpoint.json'), manifest);
|
|
130
|
-
parts.push('checkpoint.json');
|
|
131
|
-
|
|
132
|
-
writeJsonFile(path.join(dir, 'state.json'), snapshot.state ?? {});
|
|
133
|
-
parts.push('state.json');
|
|
134
|
-
writeJsonFile(path.join(dir, 'urlQuery.json'), snapshot.urlQuery ?? '');
|
|
135
|
-
parts.push('urlQuery.json');
|
|
136
|
-
writeJsonFile(path.join(dir, 'inputs.json'), snapshot.input ?? {});
|
|
137
|
-
parts.push('inputs.json');
|
|
138
|
-
writeJsonFile(path.join(dir, 'user.json'), snapshot.user ?? null);
|
|
139
|
-
parts.push('user.json');
|
|
140
|
-
writeJsonFile(path.join(dir, 'global.json'), snapshot.global ?? {});
|
|
141
|
-
parts.push('global.json');
|
|
142
|
-
|
|
143
|
-
writeEntries({ dir, subdir: 'requests', entries: snapshot.requests, parts });
|
|
144
|
-
if (!type.isNone(snapshot.apiResponses)) {
|
|
145
|
-
writeEntries({ dir, subdir: 'api', entries: snapshot.apiResponses, parts });
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return { dir, parts };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Reads a checkpoint back, tolerant of individually missing parts (a
|
|
152
|
-
// developer may hand-edit or delete files in a committed checkpoint folder) —
|
|
153
|
-
// only a missing checkpoint directory itself is an error.
|
|
154
|
-
function readCheckpoint({ name }) {
|
|
155
|
-
validateName(name);
|
|
156
|
-
const dir = path.join(getStateCheckpointsRoot(), name);
|
|
157
|
-
if (!fs.existsSync(dir)) {
|
|
158
|
-
throw new Error(`Checkpoint "${name}" not found.`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const checkpoint = readJsonFile(path.join(dir, 'checkpoint.json'), {
|
|
162
|
-
name,
|
|
163
|
-
pageId: null,
|
|
164
|
-
url: null,
|
|
165
|
-
capturedAt: null,
|
|
166
|
-
notes: null,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
name,
|
|
171
|
-
checkpoint,
|
|
172
|
-
state: readJsonFile(path.join(dir, 'state.json'), {}),
|
|
173
|
-
urlQuery: readJsonFile(path.join(dir, 'urlQuery.json'), ''),
|
|
174
|
-
input: readJsonFile(path.join(dir, 'inputs.json'), {}),
|
|
175
|
-
user: readJsonFile(path.join(dir, 'user.json'), null),
|
|
176
|
-
global: readJsonFile(path.join(dir, 'global.json'), {}),
|
|
177
|
-
requests: readEntries({ dir, subdir: 'requests' }),
|
|
178
|
-
api: readEntries({ dir, subdir: 'api' }),
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function checkpointExists({ name }) {
|
|
183
|
-
validateName(name);
|
|
184
|
-
return fs.existsSync(path.join(getStateCheckpointsRoot(), name));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Lists stored state checkpoints, most recent first. Corrupted/hand-edited
|
|
188
|
-
// checkpoints without a manifest still show up (with null fields) rather
|
|
189
|
-
// than being silently hidden, since these folders are meant to be
|
|
190
|
-
// human-editable.
|
|
191
|
-
function listStateCheckpoints() {
|
|
192
|
-
const root = getStateCheckpointsRoot();
|
|
193
|
-
if (!fs.existsSync(root)) {
|
|
194
|
-
return [];
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const checkpoints = fs
|
|
198
|
-
.readdirSync(root, { withFileTypes: true })
|
|
199
|
-
.filter((entry) => entry.isDirectory())
|
|
200
|
-
.map((entry) => {
|
|
201
|
-
const dir = path.join(root, entry.name);
|
|
202
|
-
const manifest = readJsonFile(path.join(dir, 'checkpoint.json'), {
|
|
203
|
-
name: entry.name,
|
|
204
|
-
pageId: null,
|
|
205
|
-
capturedAt: null,
|
|
206
|
-
notes: null,
|
|
207
|
-
});
|
|
208
|
-
const requestsDir = path.join(dir, 'requests');
|
|
209
|
-
const requestCount = fs.existsSync(requestsDir)
|
|
210
|
-
? fs.readdirSync(requestsDir).filter((fileName) => fileName.endsWith('.json')).length
|
|
211
|
-
: 0;
|
|
212
|
-
return {
|
|
213
|
-
name: entry.name,
|
|
214
|
-
pageId: manifest.pageId ?? null,
|
|
215
|
-
capturedAt: manifest.capturedAt ?? null,
|
|
216
|
-
notes: manifest.notes ?? null,
|
|
217
|
-
requestCount,
|
|
218
|
-
};
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
return checkpoints.sort((a, b) => {
|
|
222
|
-
if (type.isNone(a.capturedAt)) return 1;
|
|
223
|
-
if (type.isNone(b.capturedAt)) return -1;
|
|
224
|
-
return a.capturedAt < b.capturedAt ? 1 : -1;
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export {
|
|
229
|
-
checkpointExists,
|
|
230
|
-
getStateCheckpointsRoot,
|
|
231
|
-
listStateCheckpoints,
|
|
232
|
-
readCheckpoint,
|
|
233
|
-
writeCheckpoint,
|
|
234
|
-
};
|
|
@@ -1,65 +0,0 @@
|
|
|
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 YAML from 'yaml';
|
|
18
|
-
|
|
19
|
-
import { readCheckpoint } from './checkpointStore.js';
|
|
20
|
-
|
|
21
|
-
// Errors can't round-trip through YAML as Error instances (and don't need
|
|
22
|
-
// to) — @lowdefy/e2e-utils' createMockManager accepts a plain string and
|
|
23
|
-
// wraps it itself (`error instanceof Error ? error : new Error(error)`), so
|
|
24
|
-
// only the message is kept here.
|
|
25
|
-
function toMockError(error) {
|
|
26
|
-
if (!error) {
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
return error.message ?? String(error);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function toMockEntries(entries, idKey, extraFields = {}) {
|
|
33
|
-
return Object.entries(entries ?? {}).map(([id, entry]) => {
|
|
34
|
-
const error = toMockError(entry?.error);
|
|
35
|
-
const base = { [idKey]: id, ...extraFields };
|
|
36
|
-
if (error) {
|
|
37
|
-
return { ...base, error };
|
|
38
|
-
}
|
|
39
|
-
return { ...base, response: entry?.response ?? null };
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Converts a state checkpoint's recorded requests/api responses into the
|
|
44
|
-
// exact shape @lowdefy/e2e-utils' loadStaticMocks/createMockManager expect
|
|
45
|
-
// (`{ requests: [...], api: [...] }`), so a checkpoint recorded from a live
|
|
46
|
-
// dev session can seed a future AI-generated (or hand-written) e2e test's
|
|
47
|
-
// mocks file directly.
|
|
48
|
-
function checkpointToMocks({ name }) {
|
|
49
|
-
let checkpoint;
|
|
50
|
-
try {
|
|
51
|
-
checkpoint = readCheckpoint({ name });
|
|
52
|
-
} catch (error) {
|
|
53
|
-
return { error: error.message };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const pageId = checkpoint.checkpoint.pageId;
|
|
57
|
-
const requests = toMockEntries(checkpoint.requests, 'requestId', { pageId });
|
|
58
|
-
const api = toMockEntries(checkpoint.api, 'endpointId');
|
|
59
|
-
|
|
60
|
-
const yamlContent = YAML.stringify({ requests, api });
|
|
61
|
-
|
|
62
|
-
return { requests, api, yaml: yamlContent };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export default checkpointToMocks;
|
|
@@ -1,156 +0,0 @@
|
|
|
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 os from 'os';
|
|
19
|
-
import path from 'path';
|
|
20
|
-
|
|
21
|
-
import { MAX_CHECKPOINTS } from './checkpointPaths.js';
|
|
22
|
-
import createConfigCheckpoint from './createConfigCheckpoint.js';
|
|
23
|
-
import listConfigCheckpoints from './listConfigCheckpoints.js';
|
|
24
|
-
import revertConfigCheckpoint from './revertConfigCheckpoint.js';
|
|
25
|
-
|
|
26
|
-
let configDirectory;
|
|
27
|
-
let previousConfigDirectory;
|
|
28
|
-
let previousCwd;
|
|
29
|
-
|
|
30
|
-
function writeFile(relativePath, content) {
|
|
31
|
-
const filePath = path.join(configDirectory, relativePath);
|
|
32
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
33
|
-
fs.writeFileSync(filePath, content);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function readFile(relativePath) {
|
|
37
|
-
return fs.readFileSync(path.join(configDirectory, relativePath), 'utf8');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function exists(relativePath) {
|
|
41
|
-
return fs.existsSync(path.join(configDirectory, relativePath));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Checkpoint ids are timestamp-prefixed (see createConfigCheckpoint.js), so
|
|
45
|
-
// tests asserting ordering/eviction need genuinely distinct timestamps
|
|
46
|
-
// between calls. A real (short) delay is used instead of mocking the clock —
|
|
47
|
-
// this Node/Jest combination's fake timers cannot redefine `performance`.
|
|
48
|
-
function sleep(ms) {
|
|
49
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-config-checkpoints-test-'));
|
|
54
|
-
previousConfigDirectory = process.env.LOWDEFY_DIRECTORY_CONFIG;
|
|
55
|
-
process.env.LOWDEFY_DIRECTORY_CONFIG = configDirectory;
|
|
56
|
-
previousCwd = process.cwd();
|
|
57
|
-
process.chdir(configDirectory);
|
|
58
|
-
|
|
59
|
-
writeFile('lowdefy.yaml', 'name: my-app\n');
|
|
60
|
-
writeFile(path.join('pages', 'home.yaml'), 'id: home\n');
|
|
61
|
-
writeFile('.env', 'SECRET=abc\n');
|
|
62
|
-
writeFile('README.md', '# My App\n');
|
|
63
|
-
// Should never be snapshotted.
|
|
64
|
-
writeFile(path.join('node_modules', 'some-dep', 'index.js'), 'module.exports = {};\n');
|
|
65
|
-
writeFile(path.join('.git', 'HEAD'), 'ref: refs/heads/main\n');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
afterEach(() => {
|
|
69
|
-
process.chdir(previousCwd);
|
|
70
|
-
fs.rmSync(configDirectory, { recursive: true, force: true });
|
|
71
|
-
if (previousConfigDirectory === undefined) {
|
|
72
|
-
delete process.env.LOWDEFY_DIRECTORY_CONFIG;
|
|
73
|
-
} else {
|
|
74
|
-
process.env.LOWDEFY_DIRECTORY_CONFIG = previousConfigDirectory;
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('createConfigCheckpoint snapshots config-relevant files and excludes build/vcs/dependency directories', () => {
|
|
79
|
-
const result = createConfigCheckpoint({ label: 'before change' });
|
|
80
|
-
|
|
81
|
-
expect(result.fileCount).toBe(4);
|
|
82
|
-
expect(result.id).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-before-change$/);
|
|
83
|
-
|
|
84
|
-
const snapshotDirectory = path.join(configDirectory, '.lowdefy', 'checkpoints', result.id);
|
|
85
|
-
const manifest = JSON.parse(
|
|
86
|
-
fs.readFileSync(path.join(snapshotDirectory, 'manifest.json'), 'utf8')
|
|
87
|
-
);
|
|
88
|
-
expect(manifest.files.sort()).toEqual(
|
|
89
|
-
['lowdefy.yaml', path.join('pages', 'home.yaml'), '.env', 'README.md'].sort()
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
expect(fs.readFileSync(path.join(snapshotDirectory, 'lowdefy.yaml'), 'utf8')).toBe(
|
|
93
|
-
'name: my-app\n'
|
|
94
|
-
);
|
|
95
|
-
expect(fs.existsSync(path.join(snapshotDirectory, 'node_modules'))).toBe(false);
|
|
96
|
-
expect(fs.existsSync(path.join(snapshotDirectory, '.git'))).toBe(false);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test('listConfigCheckpoints returns an empty array when no checkpoints exist', () => {
|
|
100
|
-
expect(listConfigCheckpoints()).toEqual([]);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('listConfigCheckpoints lists checkpoints most recent first', async () => {
|
|
104
|
-
const first = createConfigCheckpoint({ label: 'first' });
|
|
105
|
-
await sleep(5);
|
|
106
|
-
const second = createConfigCheckpoint({ label: 'second' });
|
|
107
|
-
|
|
108
|
-
const checkpoints = listConfigCheckpoints();
|
|
109
|
-
expect(checkpoints).toEqual([
|
|
110
|
-
{ id: second.id, label: 'second', createdAt: expect.any(String), fileCount: 4 },
|
|
111
|
-
{ id: first.id, label: 'first', createdAt: expect.any(String), fileCount: 4 },
|
|
112
|
-
]);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('revertConfigCheckpoint restores modified and deleted files, and deletes files added since the checkpoint', () => {
|
|
116
|
-
const { id } = createConfigCheckpoint({ label: 'snapshot' });
|
|
117
|
-
|
|
118
|
-
// Modify an existing file.
|
|
119
|
-
writeFile('lowdefy.yaml', 'name: modified-app\n');
|
|
120
|
-
// Delete a file that was in the checkpoint.
|
|
121
|
-
fs.rmSync(path.join(configDirectory, 'pages', 'home.yaml'));
|
|
122
|
-
// Add a new config-relevant file that was not in the checkpoint.
|
|
123
|
-
writeFile(path.join('pages', 'extra.yaml'), 'id: extra\n');
|
|
124
|
-
|
|
125
|
-
const result = revertConfigCheckpoint({ id });
|
|
126
|
-
|
|
127
|
-
expect(readFile('lowdefy.yaml')).toBe('name: my-app\n');
|
|
128
|
-
expect(exists(path.join('pages', 'home.yaml'))).toBe(true);
|
|
129
|
-
expect(readFile(path.join('pages', 'home.yaml'))).toBe('id: home\n');
|
|
130
|
-
expect(exists(path.join('pages', 'extra.yaml'))).toBe(false);
|
|
131
|
-
|
|
132
|
-
expect(result.restored.sort()).toEqual(
|
|
133
|
-
['lowdefy.yaml', path.join('pages', 'home.yaml'), '.env', 'README.md'].sort()
|
|
134
|
-
);
|
|
135
|
-
expect(result.deleted).toEqual([path.join('pages', 'extra.yaml')]);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('revertConfigCheckpoint throws an actionable error when the checkpoint id does not exist', () => {
|
|
139
|
-
expect(() => revertConfigCheckpoint({ id: 'does-not-exist' })).toThrow(
|
|
140
|
-
'Config checkpoint "does-not-exist" not found.'
|
|
141
|
-
);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test('createConfigCheckpoint evicts the oldest checkpoint once more than the cap are stored', async () => {
|
|
145
|
-
for (let i = 0; i < MAX_CHECKPOINTS + 1; i += 1) {
|
|
146
|
-
createConfigCheckpoint({ label: `checkpoint-${i}` });
|
|
147
|
-
await sleep(2);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const checkpoints = listConfigCheckpoints();
|
|
151
|
-
expect(checkpoints).toHaveLength(MAX_CHECKPOINTS);
|
|
152
|
-
expect(checkpoints.some((checkpoint) => checkpoint.label === 'checkpoint-0')).toBe(false);
|
|
153
|
-
expect(
|
|
154
|
-
checkpoints.some((checkpoint) => checkpoint.label === `checkpoint-${MAX_CHECKPOINTS}`)
|
|
155
|
-
).toBe(true);
|
|
156
|
-
});
|