@lowdefy/server-dev 6.1.0 → 7.0.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/client/App.jsx +52 -24
- package/client/Inspector.jsx +73 -30
- package/client/JourneyObserver.jsx +47 -0
- package/client/Page.jsx +54 -22
- package/client/Recorder.jsx +73 -0
- package/client/Routing.jsx +37 -13
- package/client/clientBuildImports.test.mjs +2 -0
- package/client/devStreamConnections.test.mjs +40 -0
- package/client/feedback/FeedbackMount.jsx +2 -2
- package/client/feedback/FeedbackOverlay.jsx +7 -4
- package/client/feedback/feedbackStyles.js +3 -0
- package/client/journeyObserver/startJourneyObserver.js +101 -0
- package/client/journeyObserver/startJourneyObserver.test.mjs +149 -0
- package/client/openInEditor/OpenInEditorListener.jsx +2 -0
- package/client/recorder/buildEngineRecord.js +37 -0
- package/client/recorder/buildInteractionRecord.js +64 -0
- package/client/recorder/buildTraceEvent.js +57 -0
- package/client/recorder/buildTraceEvent.test.mjs +81 -0
- package/client/recorder/chunkRecords.js +69 -0
- package/client/recorder/chunkRecords.test.mjs +59 -0
- package/client/recorder/collectEventActions.js +30 -0
- package/client/recorder/collectEventEndpoints.js +40 -0
- package/client/recorder/collectEventRequests.js +39 -0
- package/client/recorder/createPairingBuffer.js +187 -0
- package/client/recorder/createPairingBuffer.test.mjs +405 -0
- package/client/recorder/createPasswordRedactor.js +84 -0
- package/client/recorder/createRecordBatcher.js +54 -0
- package/client/recorder/createRecordBatcher.test.mjs +76 -0
- package/client/recorder/createRecorder.js +216 -0
- package/client/recorder/createRecorder.test.mjs +318 -0
- package/client/recorder/createRecordingSession.js +61 -0
- package/client/recorder/createRecordingSession.test.mjs +72 -0
- package/client/recorder/diffStateWrites.js +77 -0
- package/client/recorder/diffStateWrites.test.mjs +66 -0
- package/client/recorder/isEventCall.js +30 -0
- package/client/recorder/jsonType.js +39 -0
- package/client/recorder/keyChord.js +47 -0
- package/client/recorder/keyChord.test.mjs +50 -0
- package/client/recorder/sendRecords.js +40 -0
- package/client/recorder/toJsonSafe.js +29 -0
- package/lib/client/BuildingPage.jsx +3 -1
- package/lib/client/ErrorBar.jsx +17 -13
- package/lib/client/RedirectingPage.jsx +56 -0
- package/lib/client/auth/Auth.jsx +11 -6
- package/lib/client/auth/AuthConfigured.jsx +222 -36
- package/lib/client/auth/AuthNotConfigured.js +45 -4
- package/lib/client/auth/authNotConfiguredParity.test.mjs +61 -0
- package/lib/client/auth/authTemplateSync.test.mjs +40 -0
- package/lib/client/auth/betterAuthSessionContract.test.mjs +181 -0
- package/lib/client/auth/createFetchCheckingBuild.js +45 -0
- package/lib/client/bundleBuildId.js +25 -0
- package/lib/client/collectVisibleBlockIds.js +51 -0
- package/lib/client/getCspNonce.js +25 -0
- package/lib/client/getPagePath.js +36 -0
- package/lib/client/getPagePath.test.mjs +51 -0
- package/lib/client/utils/formatErrorsForCopy.js +30 -0
- package/lib/client/utils/formatErrorsForCopy.test.mjs +39 -0
- package/lib/client/utils/getErrorBarColor.js +25 -0
- package/lib/client/utils/getErrorBarColor.test.mjs +34 -0
- package/lib/client/utils/request.js +8 -1
- package/lib/client/utils/usePageConfig.js +89 -25
- package/lib/client/utils/usePageConfig.test.mjs +198 -13
- package/lib/configTree/buildConfigTree.mjs +58 -0
- package/lib/configTree/buildConfigTree.test.mjs +77 -0
- package/lib/configTree/collectConfigText.mjs +101 -0
- package/lib/configTree/collectConfigText.test.mjs +116 -0
- package/lib/configTree/prepareScratchServer.mjs +46 -0
- package/lib/configTree/runConfigTreeBuild.mjs +88 -0
- package/lib/configTree/runConfigTreeBuild.skipPluginChecks.test.mjs +74 -0
- package/lib/configTree/runConfigTreeBuild.test.mjs +202 -0
- package/lib/docs/JourneyStepError.js +30 -0
- package/lib/docs/acquireBrowserSlot.js +30 -0
- package/lib/docs/addNestedEndpoints.js +54 -0
- package/lib/docs/addNestedEndpoints.test.mjs +185 -0
- package/lib/docs/addWriteFlags.js +77 -0
- package/lib/docs/buildEditedPages.js +50 -0
- package/lib/docs/buildWaitHeader.js +21 -0
- package/lib/docs/captureAnnotatedScreenshot.js +23 -12
- package/lib/docs/checkAgentFiles.js +148 -0
- package/lib/docs/checkAgentFiles.test.mjs +225 -0
- package/lib/docs/checkStepReferences.js +239 -0
- package/lib/docs/checkStepReferences.test.mjs +173 -0
- package/lib/docs/checkpointStore.js +3 -0
- package/lib/docs/clientErrorStore.js +18 -1
- package/lib/docs/clientErrorStore.test.mjs +46 -0
- package/lib/docs/collectExercised.js +52 -0
- package/lib/docs/createBrowserLifecycle.js +102 -0
- package/lib/docs/createBrowserLifecycle.test.mjs +212 -0
- package/lib/docs/createDocsMcpServer.js +345 -388
- package/lib/docs/createDocsMcpServer.test.mjs +644 -4
- package/lib/docs/createDocsMcpServerRunJourney.test.mjs +148 -0
- package/lib/docs/createJourneyActors.chromium.test.mjs +227 -0
- package/lib/docs/createJourneyActors.js +213 -0
- package/lib/docs/createJourneyActors.test.mjs +79 -0
- package/lib/docs/createJourneyObservations.js +80 -0
- package/lib/docs/createLeftOriginError.js +26 -0
- package/lib/docs/createNetworkCounter.js +101 -0
- package/lib/docs/dataSets/checkSharedCollectionIds.js +67 -0
- package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
- package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +679 -0
- package/lib/docs/dataSets/describeDataSetResult.js +25 -0
- package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
- package/lib/docs/dataSets/deserializeFixture.js +34 -0
- package/lib/docs/dataSets/drainSessionWork.js +37 -0
- package/lib/docs/dataSets/drainSessionWork.test.mjs +64 -0
- package/lib/docs/dataSets/duplicateIndexName.js +24 -0
- package/lib/docs/dataSets/findCollectionCollisions.js +52 -0
- package/lib/docs/dataSets/getDataStore.js +106 -0
- package/lib/docs/dataSets/getDataStore.mongodb.test.mjs +63 -0
- package/lib/docs/dataSets/getDataStore.test.mjs +195 -0
- package/lib/docs/dataSets/groupDataSetByCollection.js +44 -0
- package/lib/docs/dataSets/insertFixture.js +57 -0
- package/lib/docs/dataSets/insertGeneratedDocuments.js +41 -0
- package/lib/docs/dataSets/isDataStoreGone.js +29 -0
- package/lib/docs/dataSets/loadDataSetCollection.js +42 -0
- package/lib/docs/dataSets/mergeCollectionIndexes.js +52 -0
- package/lib/docs/dataSets/mergeCollectionIndexes.test.mjs +59 -0
- package/lib/docs/dataSets/openDataSession.js +119 -0
- package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +343 -0
- package/lib/docs/dataSets/pickDataStorePort.js +58 -0
- package/lib/docs/dataSets/pickDataStorePort.test.mjs +63 -0
- package/lib/docs/dataSets/readConnectionArtifacts.js +46 -0
- package/lib/docs/dataSets/readDataSession.js +41 -0
- package/lib/docs/dataSets/readDataSession.test.mjs +57 -0
- package/lib/docs/dataSets/readDataSet.js +68 -0
- package/lib/docs/dataSets/readDataSet.test.mjs +220 -0
- package/lib/docs/dataSets/registerDataStoreShutdown.js +47 -0
- package/lib/docs/dataSets/removeDataStoreFiles.js +32 -0
- package/lib/docs/dataSets/resolveDataSetCollection.js +46 -0
- package/lib/docs/dataSets/resolveJourneyDataSet.js +106 -0
- package/lib/docs/dataSets/resolveJourneyDataSet.test.mjs +130 -0
- package/lib/docs/dataSets/sweepOrphanDatabases.js +33 -0
- package/lib/docs/dataSets/test/app/lowdefy.yaml +203 -0
- package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
- package/lib/docs/dataSets/test/app/tests/data/shop.yaml +8 -0
- package/lib/docs/dataSets/test/buildTestApp.mjs +43 -0
- package/lib/docs/devEventBus.js +136 -0
- package/lib/docs/devEventBus.test.mjs +269 -0
- package/lib/docs/devToolDefinitions.js +529 -0
- package/lib/docs/docSources.js +21 -0
- package/lib/docs/docs.test.mjs +171 -15
- package/lib/docs/docsIndex.test.mjs +377 -0
- package/lib/docs/evalOperator.js +5 -3
- package/lib/docs/evalOperatorHeadless.js +84 -12
- package/lib/docs/evalOperatorHeadless.test.mjs +7 -1
- package/lib/docs/evalOperatorInTab.js +2 -1
- package/lib/docs/findConfig.js +95 -49
- package/lib/docs/fitResponse.js +81 -0
- package/lib/docs/fitResponse.test.mjs +128 -0
- package/lib/docs/formatFeedback.js +26 -5
- package/lib/docs/formatFeedback.test.mjs +48 -0
- package/lib/docs/getAppMap.js +7 -1
- package/lib/docs/getBrowser.js +150 -80
- package/lib/docs/getBrowser.test.mjs +407 -16
- package/lib/docs/getBuildId.js +61 -0
- package/lib/docs/getBuildId.test.mjs +84 -0
- package/lib/docs/getBuildStatus.js +54 -6
- package/lib/docs/getBuildStatus.test.mjs +133 -0
- package/lib/docs/getBuildStatusAfterEdits.js +35 -0
- package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
- package/lib/docs/getCoreDoc.js +9 -1
- package/lib/docs/getDoc.js +100 -0
- package/lib/docs/getDocsIndex.js +152 -0
- package/lib/docs/getHazards.js +115 -0
- package/lib/docs/getHazards.test.mjs +175 -0
- package/lib/docs/getMailOutboxDirectory.js +26 -0
- package/lib/docs/getOverview.js +79 -13
- package/lib/docs/getOverviewBasePath.test.mjs +37 -0
- package/lib/docs/getPageBuildStatus.js +46 -0
- package/lib/docs/getPluginDoc.js +21 -23
- package/lib/docs/getSchema.js +33 -1
- package/lib/docs/getStaleStatus.js +36 -0
- package/lib/docs/getStaleStatus.test.mjs +74 -0
- package/lib/docs/guardJourneyOrigin.js +39 -0
- package/lib/docs/inspectState.js +5 -3
- package/lib/docs/inspectStateFromTab.js +2 -2
- package/lib/docs/inspectStateHeadless.js +89 -27
- package/lib/docs/inspectStateHeadless.test.mjs +20 -1
- package/lib/docs/installHeadlessShell.failure.test.mjs +45 -0
- package/lib/docs/installHeadlessShell.js +123 -0
- package/lib/docs/installHeadlessShell.test.mjs +70 -0
- package/lib/docs/installHeadlessShell.timeout.test.mjs +72 -0
- package/lib/docs/isHeadlessShellIncomplete.js +34 -0
- package/lib/docs/isHeadlessShellIncomplete.test.mjs +63 -0
- package/lib/docs/isPageReady.js +77 -0
- package/lib/docs/isPageReady.test.mjs +235 -0
- package/lib/docs/isRunErrorEntry.js +25 -0
- package/lib/docs/journeyMutantCookie.chromium.test.mjs +150 -0
- package/lib/docs/journeyRunRecording.js +35 -0
- package/lib/docs/journeyRunRecording.test.mjs +39 -0
- package/lib/docs/keepEventStreamAlive.js +68 -0
- package/lib/docs/keepEventStreamAlive.test.mjs +77 -0
- package/lib/docs/launchBrowser.js +83 -0
- package/lib/docs/launchBrowser.test.mjs +175 -0
- package/lib/docs/listModuleDocEntries.js +69 -0
- package/lib/docs/listPackageDocFiles.js +67 -0
- package/lib/docs/listPluginDocEntries.js +111 -0
- package/lib/docs/listPlugins.js +9 -58
- package/lib/docs/listTypes.js +14 -6
- package/lib/docs/loadFrameworkHazards.js +40 -0
- package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
- package/lib/docs/loadState.js +103 -35
- package/lib/docs/makeModuleManifestDoc.js +174 -0
- package/lib/docs/matchExercisedPages.js +35 -0
- package/lib/docs/matchJourneyRoute.js +69 -0
- package/lib/docs/matchJourneyRoute.test.mjs +178 -0
- package/lib/docs/mergeNetworkSnapshots.js +49 -0
- package/lib/docs/nextJourneySequence.js +30 -0
- package/lib/docs/noBrowserError.js +22 -0
- package/lib/docs/observe/TRANSIENT_SELECTOR.js +21 -0
- package/lib/docs/observe/claimExpectedErrors.js +67 -0
- package/lib/docs/observe/createJourneyAppErrors.js +236 -0
- package/lib/docs/observe/createJourneyAppErrors.test.mjs +385 -0
- package/lib/docs/observe/evaluateInvariants.js +183 -0
- package/lib/docs/observe/evaluateInvariants.test.mjs +299 -0
- package/lib/docs/observe/findingKey.js +29 -0
- package/lib/docs/observe/hasNoEffect.js +31 -0
- package/lib/docs/observe/hasNoEffect.test.mjs +37 -0
- package/lib/docs/observe/installStepObserver.js +72 -0
- package/lib/docs/observe/isAppApiUrl.js +33 -0
- package/lib/docs/observe/parseAppApiUrl.js +37 -0
- package/lib/docs/observe/readStepObserver.js +42 -0
- package/lib/docs/observe/relativeSource.js +29 -0
- package/lib/docs/observe/resolveConfigKeySource.js +33 -0
- package/lib/docs/observe/takeJourneyEvents.js +36 -0
- package/lib/docs/observe/toAppErrorFailure.js +61 -0
- package/lib/docs/observe/usesSearchStage.js +45 -0
- package/lib/docs/observe/waitForClientErrorReports.js +31 -0
- package/lib/docs/observe/watchJourneyContext.js +75 -0
- package/lib/docs/observe/withAppErrors.js +31 -0
- package/lib/docs/observe/withAppErrors.test.mjs +101 -0
- package/lib/docs/openJourney.js +94 -0
- package/lib/docs/openJourneyEmail.js +41 -0
- package/lib/docs/openJourneyEmail.test.mjs +187 -0
- package/lib/docs/pageBlocks.js +47 -0
- package/lib/docs/pageBuildStatus.test.mjs +103 -0
- package/lib/{client/setPageId.js → docs/parseModuleSource.js} +11 -12
- package/lib/docs/rails/authEngineActions.js +53 -0
- package/lib/docs/rails/authEngineActions.test.mjs +69 -0
- package/lib/docs/rails/authEngineMethods.js +52 -0
- package/lib/docs/rails/authEngineMethods.test.mjs +36 -0
- package/lib/docs/rails/checkAuthActionsRan.js +66 -0
- package/lib/docs/rails/checkAuthActionsRan.test.mjs +149 -0
- package/lib/docs/rails/checkClickRails.js +56 -0
- package/lib/docs/rails/createDataSetRails.js +82 -0
- package/lib/docs/rails/createDataSetRails.test.mjs +172 -0
- package/lib/docs/rails/findExternalBlocks.js +147 -0
- package/lib/docs/rails/findExternalBlocks.test.mjs +254 -0
- package/lib/docs/rails/installAuthActionRecorder.js +56 -0
- package/lib/docs/rails/readAuthActionRecorder.js +32 -0
- package/lib/docs/rails/readBuiltArtifact.js +30 -0
- package/lib/docs/readConnectionPlugins.js +26 -0
- package/lib/docs/readDevAuthMode.js +32 -0
- package/lib/docs/readDocEntry.js +40 -0
- package/lib/docs/readDocTitle.js +26 -0
- package/lib/docs/readJourneyEmailMatch.js +57 -0
- package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
- package/lib/docs/readJourneySession.js +56 -0
- package/lib/docs/readMailOutbox.js +37 -0
- package/lib/docs/readPagePath.js +27 -0
- package/lib/docs/readPluginPackages.js +99 -0
- package/lib/docs/readProxyBuildWait.js +38 -0
- package/lib/docs/readRoutineSteps.js +45 -0
- package/lib/docs/registerShownTextEngine.js +118 -0
- package/lib/docs/requestRestart.js +30 -0
- package/lib/docs/requestRestart.test.mjs +49 -0
- package/lib/docs/resolvePageInstance.js +46 -0
- package/lib/docs/resolvePageInstance.test.mjs +81 -0
- package/lib/docs/resolveSource.js +11 -7
- package/lib/docs/resolveSource.test.mjs +1 -1
- package/lib/docs/resolveToolCaller.js +62 -0
- package/lib/docs/reviewPage.js +51 -0
- package/lib/docs/reviewPageBuilds.js +56 -0
- package/lib/docs/reviewPageBuilds.test.mjs +207 -0
- package/lib/docs/runCheck.js +75 -0
- package/lib/docs/runCheck.test.mjs +91 -0
- package/lib/docs/runCheckChild.js +49 -0
- package/lib/docs/runEndpoint.js +185 -0
- package/lib/docs/runEndpoint.test.mjs +509 -0
- package/lib/docs/runErrorBuffers.js +99 -0
- package/lib/docs/runErrorBuffers.test.mjs +95 -0
- package/lib/docs/runJourney.js +347 -0
- package/lib/docs/runJourney.test.mjs +2383 -0
- package/lib/docs/runJourneyAsUsers.js +44 -0
- package/lib/docs/runJourneyDataSet.test.mjs +274 -0
- package/lib/docs/runJourneySteps.js +1320 -0
- package/lib/docs/runRequest.js +100 -59
- package/lib/docs/runRequest.test.mjs +190 -0
- package/lib/docs/saveAnnotatedScreenshot.js +1 -1
- package/lib/docs/scaffoldPage.js +3 -1
- package/lib/docs/screenshotPage.js +163 -11
- package/lib/docs/screenshotPage.test.mjs +263 -1
- package/lib/docs/screenshotPageClip.test.mjs +4 -0
- package/lib/docs/searchDocs.js +82 -30
- package/lib/docs/searchDocs.test.mjs +100 -0
- package/lib/docs/searchIcons.js +90 -0
- package/lib/docs/searchIcons.test.mjs +127 -0
- package/lib/docs/selectExercised.js +39 -0
- package/lib/docs/selectFinalState.js +66 -0
- package/lib/docs/selectFinalState.test.mjs +51 -0
- package/lib/docs/serverErrorStore.js +54 -0
- package/lib/docs/serverErrorStore.test.mjs +50 -0
- package/lib/docs/setupTestFixtures.mjs +133 -39
- package/lib/docs/shownTextSelector.js +28 -0
- package/lib/docs/snapshotState.js +13 -5
- package/lib/docs/splitSearchTerms.js +62 -0
- package/lib/docs/splitSearchTerms.test.mjs +50 -0
- package/lib/docs/stateCheckpoints.test.mjs +77 -0
- package/lib/docs/tabAvailable.js +6 -5
- package/lib/docs/tabChannel.js +127 -21
- package/lib/docs/tabChannel.test.mjs +187 -29
- package/lib/docs/unsettledPageNote.js +24 -0
- package/lib/docs/validateJourneyMail.js +36 -0
- package/lib/docs/validateJourneyTimeout.js +38 -0
- package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
- package/lib/docs/validateRecordingParam.js +42 -0
- package/lib/docs/validateStateSelection.js +33 -0
- package/lib/docs/validateStateSelection.test.mjs +33 -0
- package/lib/docs/validateViewport.js +51 -0
- package/lib/docs/validateViewport.test.mjs +54 -0
- package/lib/docs/waitForBuild.js +50 -0
- package/lib/docs/waitForBuild.test.mjs +75 -0
- package/lib/docs/waitForJourneyEmail.js +77 -0
- package/lib/docs/withBrowserSlot.js +45 -0
- package/lib/docs/withBrowserSlot.test.mjs +75 -0
- package/lib/docs/withDataSession.js +53 -0
- package/lib/server/applyDataSetRedirect.js +83 -0
- package/lib/server/applyDataSetRedirect.test.mjs +340 -0
- package/lib/server/applyDataSetRedirectTenantReach.test.mjs +204 -0
- package/lib/server/applyDataSetRedirectWalled.test.mjs +131 -0
- package/lib/server/auth/createSystemContext.js +73 -0
- package/lib/server/auth/{getAuthConfig.js → getAuth.js} +15 -9
- package/lib/server/auth/getHeadlessUser.js +9 -8
- package/lib/server/auth/getHeadlessUser.test.mjs +41 -0
- package/lib/server/auth/getMockUser.js +18 -14
- package/lib/server/auth/{session.js → getStrategies.js} +17 -16
- package/lib/server/auth/headlessUser.js +3 -2
- package/lib/server/auth/journeyActor.js +35 -0
- package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
- package/lib/server/checkPageRecord.js +44 -0
- package/lib/server/contextMapBudget.js +24 -0
- package/lib/server/contextMapBudget.test.mjs +98 -0
- package/lib/server/continueJitKeys.js +28 -0
- package/lib/server/continueJitKeys.test.mjs +43 -0
- package/lib/server/createDataSessionEndedError.js +33 -0
- package/lib/server/createDataSetRefusal.js +26 -0
- package/lib/server/createLowdefyContext.js +168 -41
- package/lib/server/createLowdefyContext.test.mjs +328 -8
- package/lib/server/getClientAddress.js +51 -0
- package/lib/server/getClientAddress.test.mjs +56 -0
- package/lib/server/getPageJitEnrichment.test.mjs +29 -11
- package/lib/server/hashConfigContent.js +26 -0
- package/lib/server/importAppCode.test.mjs +57 -0
- package/lib/server/isRoutineStep.js +27 -0
- package/lib/server/jitPageBuilder.js +309 -126
- package/lib/server/jitPageBuilder.skipPluginChecks.test.mjs +78 -0
- package/lib/server/jitPageBuilder.test.mjs +218 -0
- package/lib/server/journeyCookies/JOURNEY_COOKIES.js +37 -0
- package/lib/server/journeyCookies/forwardJourneyCookies.js +46 -0
- package/lib/server/journeyCookies/readJourneyCookie.js +52 -0
- package/lib/server/journeyCookies/writeJourneyCookie.js +46 -0
- package/lib/server/journeyCookies.js +22 -0
- package/lib/server/journeyCookies.test.mjs +145 -0
- package/lib/server/loadDynamicJsMap.js +47 -0
- package/lib/server/log/createHandleError.js +38 -2
- package/lib/server/log/createHandleError.test.mjs +125 -2
- package/lib/server/log/createLogger.js +3 -0
- package/lib/server/log/createLogger.test.mjs +15 -0
- package/lib/server/log/logRequest.js +2 -3
- package/lib/server/mutantDataSetWrappers.test.mjs +112 -0
- package/lib/server/mutants/applyMutant.js +39 -0
- package/lib/server/mutants/artifactPatterns.js +25 -0
- package/lib/server/mutants/buildAnchor.js +45 -0
- package/lib/server/mutants/collectSharedIds.js +41 -0
- package/lib/server/mutants/configPathSegments.js +38 -0
- package/lib/server/mutants/createMutantReadConfigFile.js +42 -0
- package/lib/server/mutants/describeEvent.js +22 -0
- package/lib/server/mutants/enumerateArtifact.js +56 -0
- package/lib/server/mutants/findNodeByKey.js +45 -0
- package/lib/server/mutants/findNodeByPointer.js +49 -0
- package/lib/server/mutants/getArtifactKind.js +32 -0
- package/lib/server/mutants/groupMutantCopies.js +56 -0
- package/lib/server/mutants/hashArtifact.js +25 -0
- package/lib/server/mutants/hashMutantTarget.js +34 -0
- package/lib/server/mutants/listMutants.js +122 -0
- package/lib/server/mutants/listMutants.test.mjs +502 -0
- package/lib/server/mutants/mutantId.js +30 -0
- package/lib/server/mutants/mutantRuns.js +57 -0
- package/lib/server/mutants/mutantRuns.test.mjs +136 -0
- package/lib/server/mutants/operators/dropAction.js +47 -0
- package/lib/server/mutants/operators/dropBlock.js +44 -0
- package/lib/server/mutants/operators/dropPayload.js +52 -0
- package/lib/server/mutants/operators/dropStep.js +44 -0
- package/lib/server/mutants/operators/flipVisible.js +53 -0
- package/lib/server/mutants/operators/index.js +37 -0
- package/lib/server/mutants/operators/retargetLink.js +72 -0
- package/lib/server/mutants/operators/skipValidate.js +77 -0
- package/lib/server/mutants/operators/swapIf.js +112 -0
- package/lib/server/mutants/operators.test.mjs +396 -0
- package/lib/server/mutants/positionFreePath.js +38 -0
- package/lib/server/mutants/removeFromParent.js +28 -0
- package/lib/server/mutants/test/fixtures.mjs +173 -0
- package/lib/server/mutants/test/keyArtifact.mjs +63 -0
- package/lib/server/mutants/validateMutantParam.js +69 -0
- package/lib/server/mutants/validateMutantsBody.js +88 -0
- package/lib/server/mutants/walkArtifact.js +179 -0
- package/lib/server/pageBuildRecords.js +131 -0
- package/lib/server/pageBuildRecords.test.mjs +258 -0
- package/lib/server/pageCache.mjs +22 -7
- package/lib/server/pageCache.test.mjs +22 -7
- package/lib/server/pageRebuilds.test.mjs +543 -0
- package/lib/server/pruneJitMaps.js +36 -0
- package/lib/server/pruneJitMaps.test.mjs +55 -0
- package/lib/server/readInstanceRecord.js +33 -0
- package/lib/server/readInstanceRecord.test.mjs +99 -0
- package/lib/server/readMergedMaps.js +86 -0
- package/lib/server/readMergedMaps.test.mjs +79 -0
- package/lib/server/recording/appendRecords.js +33 -0
- package/lib/server/recording/errorRecordingStamp.js +34 -0
- package/lib/server/recording/errorRecordingStamp.test.mjs +31 -0
- package/lib/server/recording/isRecordingEnabled.js +31 -0
- package/lib/server/recording/readRecordingCookie.js +71 -0
- package/lib/server/recording/readRecordingCookie.test.mjs +73 -0
- package/lib/server/recording/recordingCookiePayload.js +33 -0
- package/lib/server/recording/servedBuilds.js +46 -0
- package/lib/server/recording/servedBuilds.test.mjs +42 -0
- package/lib/server/sinkExternalConnections.js +95 -0
- package/lib/server/sinkExternalConnections.test.mjs +135 -0
- package/lib/server/skipStaleMapWrites.js +62 -0
- package/lib/server/skipStaleMapWrites.test.mjs +113 -0
- package/lib/server/startIdleGc.js +67 -0
- package/lib/server/startIdleGc.test.mjs +138 -0
- package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
- package/lib/server/trackSessionWork.js +30 -0
- package/lib/vite/createDevServerExclude.js +40 -0
- package/lib/vite/createDevServerExclude.test.mjs +79 -0
- package/manager/getContext.mjs +24 -2
- package/manager/processes/initialBuild.mjs +14 -0
- package/manager/processes/initialBuild.test.mjs +101 -0
- package/manager/processes/lowdefyBuild.mjs +30 -7
- package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +62 -0
- package/manager/processes/lowdefyBuild.test.mjs +136 -1
- package/manager/processes/optimizeDependencies.mjs +59 -0
- package/manager/processes/restartServer.mjs +34 -6
- package/manager/processes/restartServer.test.mjs +162 -0
- package/manager/processes/shutdownServer.mjs +11 -0
- package/manager/processes/shutdownServer.test.mjs +45 -0
- package/manager/processes/startFirstServer.mjs +46 -0
- package/manager/processes/startFirstServer.test.mjs +326 -0
- package/manager/processes/startMailSink.mjs +135 -0
- package/manager/processes/startMailSink.test.mjs +146 -0
- package/manager/processes/startProxy.mjs +129 -12
- package/manager/processes/startProxy.test.mjs +366 -0
- package/manager/processes/startServer.mjs +47 -6
- package/manager/processes/startServer.test.mjs +155 -0
- package/manager/processes/startWatchers.mjs +6 -4
- package/manager/processes/syncServer.mjs +69 -0
- package/manager/processes/syncServer.test.mjs +141 -0
- package/manager/processes/waitForDevServer.mjs +48 -0
- package/manager/processes/waitForDevServer.test.mjs +112 -0
- package/manager/run.mjs +128 -43
- package/manager/utils/BatchChanges.mjs +21 -5
- package/manager/utils/BatchChanges.test.mjs +77 -0
- package/manager/utils/acquireDevInstance.mjs +99 -0
- package/manager/utils/acquireDevInstance.test.mjs +160 -0
- package/manager/utils/createBuildActivity.mjs +67 -0
- package/manager/utils/createBuildActivity.test.mjs +58 -0
- package/manager/utils/createChangeSignal.mjs +39 -0
- package/manager/utils/createChangeSignal.test.mjs +62 -0
- package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
- package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
- package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
- package/manager/utils/createDotPathIgnore.mjs +44 -0
- package/manager/utils/createDotPathIgnore.test.mjs +65 -0
- package/manager/utils/createIdleGc.mjs +51 -0
- package/manager/utils/createIdleGc.test.mjs +77 -0
- package/manager/utils/createRequestActivity.mjs +100 -0
- package/manager/utils/createRequestActivity.test.mjs +121 -0
- package/manager/utils/createServerArtifactTracker.mjs +83 -0
- package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
- package/manager/utils/createServerEnv.mjs +39 -0
- package/manager/utils/filesHaveSameBytes.mjs +41 -0
- package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
- package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
- package/manager/utils/importFresh.mjs +46 -0
- package/manager/utils/importFresh.test.mjs +72 -0
- package/manager/utils/importFreshWorker.mjs +20 -0
- package/manager/utils/killTaggedBrowser.mjs +35 -0
- package/manager/utils/pagePathChanged.mjs +56 -0
- package/manager/utils/pruneRecordings.mjs +85 -0
- package/manager/utils/pruneRecordings.test.mjs +117 -0
- package/manager/utils/publishBuildDirectory.mjs +121 -0
- package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
- package/manager/utils/publishBuildDirectory.test.mjs +262 -0
- package/manager/utils/readBasePath.mjs +27 -0
- package/manager/utils/readBasePath.test.mjs +43 -0
- package/manager/utils/readBuildStatusWait.mjs +67 -0
- package/manager/utils/readManagerVersion.mjs +30 -0
- package/manager/utils/readManagerVersion.test.mjs +35 -0
- package/manager/utils/readPluginDefinitions.mjs +36 -0
- package/manager/utils/resolveDevAuthUrl.mjs +58 -0
- package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
- package/manager/utils/resolvePorts.mjs +49 -0
- package/manager/utils/setupWatcher.mjs +19 -11
- package/manager/utils/startRecordingPruner.mjs +45 -0
- package/manager/utils/startRecordingPruner.test.mjs +66 -0
- package/manager/utils/waitForChildExit.mjs +53 -0
- package/manager/utils/waitForChildExit.test.mjs +70 -0
- package/manager/utils/waitForServer.mjs +44 -0
- package/manager/utils/writeBuildStatus.mjs +1 -1
- package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
- package/manager/watchers/envWatcher.mjs +9 -2
- package/manager/watchers/envWatcher.test.mjs +87 -0
- package/manager/watchers/listServerSidePackages.mjs +47 -0
- package/manager/watchers/listServerSidePackages.test.mjs +38 -0
- package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
- package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
- package/manager/watchers/pluginSourceWatcher.mjs +79 -0
- package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
- package/manager/watchers/restartRequestWatcher.mjs +71 -0
- package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
- package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
- package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
- package/manager/watchers/serverPackageWatcher.mjs +38 -0
- package/package.json +60 -47
- package/package.original.json +60 -47
- package/src/app.js +131 -20
- package/src/html/renderDevPage.js +63 -11
- package/src/html/renderDevPage.test.mjs +138 -0
- package/src/lib/getRequestPath.js +29 -0
- package/src/middleware/createSameOriginGuard.js +37 -0
- package/src/middleware/createSameOriginGuard.test.mjs +102 -0
- package/src/middleware/errorHandler.js +51 -1
- package/src/middleware/errorHandler.test.mjs +173 -3
- package/src/middleware/isRebindingSafeHost.js +44 -0
- package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
- package/src/middleware/localDevToolsOnly.js +38 -0
- package/src/middleware/localDevToolsOnly.test.mjs +61 -0
- package/src/middleware/noStore.js +27 -0
- package/src/middleware/noStore.test.mjs +48 -0
- package/src/middleware/requestTimeout.js +38 -0
- package/src/middleware/staleFlag.js +76 -0
- package/src/middleware/staleFlag.test.mjs +127 -0
- package/src/middleware/stampCaller.js +38 -0
- package/src/middleware/stampCaller.test.mjs +64 -0
- package/src/routes/agent.js +15 -3
- package/src/routes/auth.js +29 -20
- package/src/routes/auth.test.mjs +30 -56
- package/src/routes/clientError.js +13 -13
- package/src/routes/cron.js +3 -1
- package/src/routes/cronForward.js +3 -1
- package/src/routes/detached.js +22 -9
- package/src/routes/devInspect.js +17 -8
- package/src/routes/devInspect.test.mjs +44 -7
- package/src/routes/devRecording.js +113 -0
- package/src/routes/devRecording.test.mjs +253 -0
- package/src/routes/docs/buildStatus.js +10 -2
- package/src/routes/docs/buildStatus.test.mjs +46 -0
- package/src/routes/docs/content.js +5 -3
- package/src/routes/docs/evalOperator.js +15 -1
- package/src/routes/docs/events.js +50 -0
- package/src/routes/docs/events.test.mjs +117 -0
- package/src/routes/docs/find.js +9 -1
- package/src/routes/docs/icons.js +30 -0
- package/src/routes/docs/inspectState.js +16 -1
- package/src/routes/docs/journey.js +194 -0
- package/src/routes/docs/journey.test.mjs +401 -0
- package/src/routes/docs/journeyRefusals.test.mjs +71 -0
- package/src/routes/docs/loadState.js +4 -2
- package/src/routes/docs/mcp.js +34 -3
- package/src/routes/docs/mcp.test.mjs +130 -0
- package/src/routes/docs/mutants.js +78 -0
- package/src/routes/docs/mutants.test.mjs +180 -0
- package/src/routes/docs/parsePathParamsParam.js +41 -0
- package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
- package/src/routes/docs/parseUserParam.js +27 -21
- package/src/routes/docs/parseUserParam.test.mjs +17 -5
- package/src/routes/docs/restart.js +35 -0
- package/src/routes/docs/restart.test.mjs +64 -0
- package/src/routes/docs/runEndpoint.js +54 -0
- package/src/routes/docs/runEndpoint.test.mjs +165 -0
- package/src/routes/docs/runRequest.js +25 -8
- package/src/routes/docs/runRequest.test.mjs +92 -0
- package/src/routes/docs/screenshot.js +50 -2
- package/src/routes/docs/screenshot.test.mjs +117 -0
- package/src/routes/docs/search.js +15 -2
- package/src/routes/docs/snapshotState.js +17 -3
- package/src/routes/endpoints.js +27 -26
- package/src/routes/endpointsTextAnswer.test.mjs +77 -0
- package/src/routes/feedback.js +10 -13
- package/src/routes/jitPage.js +99 -35
- package/src/routes/jitPage.test.mjs +248 -0
- package/src/routes/mcp.js +82 -5
- package/src/routes/mcp.test.mjs +215 -0
- package/src/routes/mcpMemberToken.test.mjs +125 -0
- package/src/routes/mcpProtectedResourceMetadata.js +34 -0
- package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
- package/src/routes/mountDevPageRoutes.js +25 -0
- package/src/routes/mountDevPageRoutes.test.mjs +30 -0
- package/src/routes/mountOauthDiscovery.js +44 -0
- package/src/routes/mountOauthDiscovery.test.mjs +79 -0
- package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
- package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
- package/src/routes/reload.js +16 -0
- package/src/routes/request.js +6 -4
- package/src/routes/request.test.mjs +9 -5
- package/src/routes/requestBody.test.mjs +159 -0
- package/src/routes/usage.js +6 -2
- package/src/routes/user.js +27 -0
- package/src/routes/websocket.js +29 -8
- package/src/routes/websocket.test.mjs +65 -0
- package/src/routes/wellKnownFallback.js +30 -0
- package/src/routes/wellKnownFallback.test.mjs +32 -0
- package/src/websocket/devWebSocket.js +41 -26
- package/src/websocket/devWebSocket.test.mjs +157 -0
- package/vite.config.js +53 -23
- package/lib/server/auth/getDevSession.js +0 -70
- package/lib/server/auth/getDevSession.test.mjs +0 -138
- package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
- package/manager/watchers/moduleBuildWatcher.mjs +0 -79
- package/manager/watchers/serverArtifactWatcher.mjs +0 -120
- package/src/routes/apiPage.js +0 -37
|
@@ -0,0 +1,401 @@
|
|
|
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 { jest } from '@jest/globals';
|
|
18
|
+
|
|
19
|
+
const mockRunJourney = jest.fn();
|
|
20
|
+
|
|
21
|
+
jest.unstable_mockModule('../../../lib/docs/runJourney.js', () => ({
|
|
22
|
+
default: mockRunJourney,
|
|
23
|
+
}));
|
|
24
|
+
jest.unstable_mockModule('../../../lib/build/config.js', () => ({
|
|
25
|
+
default: { basePath: '/app' },
|
|
26
|
+
}));
|
|
27
|
+
const mockGetBuildId = jest.fn(() => 'build-1');
|
|
28
|
+
jest.unstable_mockModule('../../../lib/docs/getBuildId.js', () => ({
|
|
29
|
+
default: mockGetBuildId,
|
|
30
|
+
}));
|
|
31
|
+
const { readMutantRun } = await import('../../../lib/server/mutants/mutantRuns.js');
|
|
32
|
+
const { journeyActorToken } = await import('../../../lib/server/auth/journeyActor.js');
|
|
33
|
+
|
|
34
|
+
const { default: docsJourneyHandler } = await import('./journey.js');
|
|
35
|
+
|
|
36
|
+
function createContext(body) {
|
|
37
|
+
const request = new Request('http://localhost:3227/lowdefy-docs/journey', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'content-type': 'application/json' },
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
});
|
|
42
|
+
const json = jest.fn((data, status) => ({ data, status: status ?? 200 }));
|
|
43
|
+
return { req: { url: request.url, json: () => request.json() }, json };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
jest.clearAllMocks();
|
|
48
|
+
mockRunJourney.mockResolvedValue({
|
|
49
|
+
pageId: 'form',
|
|
50
|
+
passed: true,
|
|
51
|
+
steps: [],
|
|
52
|
+
screenshots: [{ name: 'after', data: 'cG5n', mimeType: 'image/png' }],
|
|
53
|
+
state: {},
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('docsJourneyHandler runs the journey against the request origin and returns the result', async () => {
|
|
58
|
+
const c = createContext({
|
|
59
|
+
pageId: 'form',
|
|
60
|
+
steps: [{ click: 'submit' }],
|
|
61
|
+
user: { roles: ['admin'] },
|
|
62
|
+
urlQuery: { id: '1' },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const result = await docsJourneyHandler(c);
|
|
66
|
+
|
|
67
|
+
expect(mockRunJourney).toHaveBeenCalledWith({
|
|
68
|
+
origin: 'http://localhost:3227',
|
|
69
|
+
pageId: 'form',
|
|
70
|
+
steps: [{ click: 'submit' }],
|
|
71
|
+
user: { roles: ['admin'] },
|
|
72
|
+
urlQuery: { id: '1' },
|
|
73
|
+
stepTimeout: undefined,
|
|
74
|
+
basePath: '/app',
|
|
75
|
+
by: 'test',
|
|
76
|
+
});
|
|
77
|
+
expect(result.status).toBe(200);
|
|
78
|
+
expect(result.data.passed).toBe(true);
|
|
79
|
+
expect(result.data.screenshots).toEqual([{ name: 'after', data: 'cG5n', mimeType: 'image/png' }]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('docsJourneyHandler returns a failed journey as 200 data', async () => {
|
|
83
|
+
mockRunJourney.mockResolvedValue({
|
|
84
|
+
pageId: 'form',
|
|
85
|
+
passed: false,
|
|
86
|
+
steps: [{ index: 0, step: { click: 'nope' }, status: 'failed', durationMs: 12 }],
|
|
87
|
+
failure: { index: 0, step: { click: 'nope' }, expected: 'x', actual: 'y', message: 'm' },
|
|
88
|
+
screenshots: [],
|
|
89
|
+
state: {},
|
|
90
|
+
});
|
|
91
|
+
const c = createContext({ pageId: 'form', steps: [{ click: 'nope' }] });
|
|
92
|
+
|
|
93
|
+
const result = await docsJourneyHandler(c);
|
|
94
|
+
|
|
95
|
+
expect(result.status).toBe(200);
|
|
96
|
+
expect(result.data.passed).toBe(false);
|
|
97
|
+
expect(result.data.failure.index).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('docsJourneyHandler returns 400 when pageId is missing', async () => {
|
|
101
|
+
const c = createContext({ steps: [] });
|
|
102
|
+
|
|
103
|
+
const result = await docsJourneyHandler(c);
|
|
104
|
+
|
|
105
|
+
expect(result.status).toBe(400);
|
|
106
|
+
expect(result.data.error).toMatch(/requires a "pageId" string/);
|
|
107
|
+
expect(mockRunJourney).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('docsJourneyHandler returns 400 when steps is not an array', async () => {
|
|
111
|
+
const c = createContext({ pageId: 'form', steps: { click: 'a' } });
|
|
112
|
+
|
|
113
|
+
const result = await docsJourneyHandler(c);
|
|
114
|
+
|
|
115
|
+
expect(result.status).toBe(400);
|
|
116
|
+
expect(result.data.error).toMatch(/requires "steps" to be an array/);
|
|
117
|
+
expect(mockRunJourney).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('docsJourneyHandler returns 400 naming an unknown step', async () => {
|
|
121
|
+
const c = createContext({ pageId: 'form', steps: [{ hover: 'a' }] });
|
|
122
|
+
|
|
123
|
+
const result = await docsJourneyHandler(c);
|
|
124
|
+
|
|
125
|
+
expect(result.status).toBe(400);
|
|
126
|
+
expect(result.data.error).toEqual(
|
|
127
|
+
'Step 0: Unknown journey step "hover". Steps are: click, open, fill, select, press, back, goto, email, as, wait, screenshot, expect.'
|
|
128
|
+
);
|
|
129
|
+
expect(mockRunJourney).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('docsJourneyHandler returns 400 when urlQuery is not an object', async () => {
|
|
133
|
+
const c = createContext({ pageId: 'form', steps: [], urlQuery: 'id=1' });
|
|
134
|
+
|
|
135
|
+
const result = await docsJourneyHandler(c);
|
|
136
|
+
|
|
137
|
+
expect(result.status).toBe(400);
|
|
138
|
+
expect(result.data.error).toMatch(/"urlQuery" param must be an object/);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('docsJourneyHandler passes the state option through to the runner', async () => {
|
|
142
|
+
const c = createContext({ pageId: 'form', steps: [], state: ['form.name'] });
|
|
143
|
+
|
|
144
|
+
await docsJourneyHandler(c);
|
|
145
|
+
|
|
146
|
+
expect(mockRunJourney).toHaveBeenCalledWith(expect.objectContaining({ state: ['form.name'] }));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('docsJourneyHandler returns 400 when the state option is malformed', async () => {
|
|
150
|
+
const c = createContext({ pageId: 'form', steps: [], state: 'form.name' });
|
|
151
|
+
|
|
152
|
+
const result = await docsJourneyHandler(c);
|
|
153
|
+
|
|
154
|
+
expect(result.status).toBe(400);
|
|
155
|
+
expect(result.data.error).toMatch(
|
|
156
|
+
/"state" option must be true, false or an array of state paths/
|
|
157
|
+
);
|
|
158
|
+
expect(mockRunJourney).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('docsJourneyHandler returns 400 when user is malformed', async () => {
|
|
162
|
+
const c = createContext({ pageId: 'form', steps: [], user: 5 });
|
|
163
|
+
|
|
164
|
+
const result = await docsJourneyHandler(c);
|
|
165
|
+
|
|
166
|
+
expect(result.status).toBe(400);
|
|
167
|
+
expect(result.data.error).toMatch(/must be a user object/);
|
|
168
|
+
expect(mockRunJourney).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('docsJourneyHandler refuses a list of users with 400: one POST is one run', async () => {
|
|
172
|
+
const c = createContext({ pageId: 'form', steps: [], data: 'crm', user: ['admin', 'member'] });
|
|
173
|
+
|
|
174
|
+
const result = await docsJourneyHandler(c);
|
|
175
|
+
|
|
176
|
+
expect(result.status).toBe(400);
|
|
177
|
+
expect(result.data.error).toBe(
|
|
178
|
+
'POST /lowdefy-docs/journey runs one user. For a list of data set users, post once per user, or use lowdefy test or lowdefy_run_journey, which run the journey once as each. Received ["admin","member"].'
|
|
179
|
+
);
|
|
180
|
+
expect(mockRunJourney).not.toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('docsJourneyHandler passes user none through so the journey signs in through the app', async () => {
|
|
184
|
+
const c = createContext({ pageId: 'login', steps: [{ goto: 'dashboard' }], user: 'none' });
|
|
185
|
+
|
|
186
|
+
const result = await docsJourneyHandler(c);
|
|
187
|
+
|
|
188
|
+
expect(result.status).toBe(200);
|
|
189
|
+
expect(mockRunJourney).toHaveBeenCalledWith(expect.objectContaining({ user: 'none' }));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('docsJourneyHandler passes the journey timeout to the runner as the step timeout', async () => {
|
|
193
|
+
const c = createContext({ pageId: 'form', steps: [], timeout: 20000 });
|
|
194
|
+
|
|
195
|
+
await docsJourneyHandler(c);
|
|
196
|
+
|
|
197
|
+
expect(mockRunJourney).toHaveBeenCalledWith(expect.objectContaining({ stepTimeout: 20000 }));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('docsJourneyHandler returns 400 for a timeout outside 1 to 60000 ms', async () => {
|
|
201
|
+
const c = createContext({ pageId: 'form', steps: [], timeout: 120000 });
|
|
202
|
+
|
|
203
|
+
const result = await docsJourneyHandler(c);
|
|
204
|
+
|
|
205
|
+
expect(result.status).toBe(400);
|
|
206
|
+
expect(result.data.error).toMatch(/"timeout" must be a whole number of milliseconds/);
|
|
207
|
+
expect(mockRunJourney).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('docsJourneyHandler returns 502 when the journey could not run', async () => {
|
|
211
|
+
mockRunJourney.mockResolvedValue({ error: 'No Chromium available.' });
|
|
212
|
+
const c = createContext({ pageId: 'form', steps: [] });
|
|
213
|
+
|
|
214
|
+
const result = await docsJourneyHandler(c);
|
|
215
|
+
|
|
216
|
+
expect(result.status).toBe(502);
|
|
217
|
+
expect(result.data.error).toEqual('No Chromium available.');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('docsJourneyHandler passes data and a user name through to the runner', async () => {
|
|
221
|
+
const c = createContext({
|
|
222
|
+
pageId: 'form',
|
|
223
|
+
steps: [{ click: 'submit' }],
|
|
224
|
+
data: 'staging-sample',
|
|
225
|
+
user: 'member',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const result = await docsJourneyHandler(c);
|
|
229
|
+
|
|
230
|
+
expect(result.status).toBe(200);
|
|
231
|
+
expect(mockRunJourney).toHaveBeenCalledWith(
|
|
232
|
+
expect.objectContaining({ data: 'staging-sample', user: 'member' })
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('docsJourneyHandler answers 400 when the runner refuses the journey before a browser opens', async () => {
|
|
237
|
+
mockRunJourney.mockResolvedValue({
|
|
238
|
+
error: 'Data set "staging-sample" declares no user "outsider". Declared: owner.',
|
|
239
|
+
refused: true,
|
|
240
|
+
});
|
|
241
|
+
const c = createContext({
|
|
242
|
+
pageId: 'form',
|
|
243
|
+
steps: [{ click: 'submit' }],
|
|
244
|
+
data: 'staging-sample',
|
|
245
|
+
user: 'outsider',
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const result = await docsJourneyHandler(c);
|
|
249
|
+
|
|
250
|
+
expect(result.status).toBe(400);
|
|
251
|
+
expect(result.data).toEqual({
|
|
252
|
+
error: 'Data set "staging-sample" declares no user "outsider". Declared: owner.',
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('docsJourneyHandler refuses a data param that is not a string with 400', async () => {
|
|
257
|
+
const c = createContext({ pageId: 'form', steps: [{ click: 'submit' }], data: { name: 'x' } });
|
|
258
|
+
|
|
259
|
+
const result = await docsJourneyHandler(c);
|
|
260
|
+
|
|
261
|
+
expect(result.status).toBe(400);
|
|
262
|
+
expect(result.data.error).toMatch('The "data" param must be a data set name string.');
|
|
263
|
+
expect(mockRunJourney).not.toHaveBeenCalled();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const mutant = {
|
|
267
|
+
buildId: 'build-1',
|
|
268
|
+
artifact: 'pages/form.json',
|
|
269
|
+
key: 'k1_5',
|
|
270
|
+
arg: null,
|
|
271
|
+
operator: 'drop-block',
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
test('docsJourneyHandler opens a mutant run, passes its cookie to every actor and closes it after', async () => {
|
|
275
|
+
let runDuringJourney;
|
|
276
|
+
mockRunJourney.mockImplementation(async ({ mutantCookie }) => {
|
|
277
|
+
runDuringJourney = readMutantRun(`lowdefy_journey_mutant=${journeyActorToken}.${mutantCookie}`);
|
|
278
|
+
runDuringJourney.applied = 2;
|
|
279
|
+
return { pageId: 'form', passed: true, steps: [], screenshots: [], state: {} };
|
|
280
|
+
});
|
|
281
|
+
const c = createContext({ pageId: 'form', steps: [{ click: 'submit' }], mutant });
|
|
282
|
+
const result = await docsJourneyHandler(c);
|
|
283
|
+
expect(result.status).toBe(200);
|
|
284
|
+
expect(runDuringJourney.mutant).toEqual(mutant);
|
|
285
|
+
expect(result.data.mutant).toEqual({ id: runDuringJourney.id, applied: 2, misses: [] });
|
|
286
|
+
expect(
|
|
287
|
+
readMutantRun(`lowdefy_journey_mutant=${journeyActorToken}.${runDuringJourney.id}`)
|
|
288
|
+
).toBeNull();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('docsJourneyHandler opens the run with the mutant pointer and node hash it was sent', async () => {
|
|
292
|
+
let runDuringJourney;
|
|
293
|
+
mockRunJourney.mockImplementation(async ({ mutantCookie }) => {
|
|
294
|
+
runDuringJourney = readMutantRun(`lowdefy_journey_mutant=${journeyActorToken}.${mutantCookie}`);
|
|
295
|
+
return { pageId: 'form', passed: true, steps: [], screenshots: [], state: {} };
|
|
296
|
+
});
|
|
297
|
+
const withPointer = { ...mutant, pointer: ['slots', 'content', 'blocks', 0], nodeHash: 'abc' };
|
|
298
|
+
const c = createContext({ pageId: 'form', steps: [{ click: 'submit' }], mutant: withPointer });
|
|
299
|
+
const result = await docsJourneyHandler(c);
|
|
300
|
+
expect(result.status).toBe(200);
|
|
301
|
+
expect(runDuringJourney.mutant).toEqual(withPointer);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('docsJourneyHandler refuses a mutant pointer that is not keys and indexes with 400', async () => {
|
|
305
|
+
const c = createContext({
|
|
306
|
+
pageId: 'form',
|
|
307
|
+
steps: [{ click: 'submit' }],
|
|
308
|
+
mutant: { ...mutant, pointer: [{ a: 1 }] },
|
|
309
|
+
});
|
|
310
|
+
const result = await docsJourneyHandler(c);
|
|
311
|
+
expect(result.status).toBe(400);
|
|
312
|
+
expect(result.data.error).toMatch('"mutant.pointer" must be an array of keys and indexes');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('docsJourneyHandler closes the mutant run when the journey throws', async () => {
|
|
316
|
+
let cookie;
|
|
317
|
+
mockRunJourney.mockImplementation(async ({ mutantCookie }) => {
|
|
318
|
+
cookie = mutantCookie;
|
|
319
|
+
throw new Error('browser crashed');
|
|
320
|
+
});
|
|
321
|
+
const c = createContext({ pageId: 'form', steps: [{ click: 'submit' }], mutant });
|
|
322
|
+
await expect(docsJourneyHandler(c)).rejects.toThrow('browser crashed');
|
|
323
|
+
expect(readMutantRun(`lowdefy_journey_mutant=${journeyActorToken}.${cookie}`)).toBeNull();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('docsJourneyHandler refuses a mutant listed against another build with 409 and stale', async () => {
|
|
327
|
+
mockGetBuildId.mockReturnValueOnce('build-2');
|
|
328
|
+
const c = createContext({ pageId: 'form', steps: [{ click: 'submit' }], mutant });
|
|
329
|
+
const result = await docsJourneyHandler(c);
|
|
330
|
+
expect(result.status).toBe(409);
|
|
331
|
+
expect(result.data.stale).toBe(true);
|
|
332
|
+
expect(mockRunJourney).not.toHaveBeenCalled();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test.each([['../x.json'], ['connections/a.json'], ['pages/../secrets.json'], [5]])(
|
|
336
|
+
'docsJourneyHandler refuses a mutant artifact %j with 400',
|
|
337
|
+
async (artifact) => {
|
|
338
|
+
const c = createContext({
|
|
339
|
+
pageId: 'form',
|
|
340
|
+
steps: [{ click: 'submit' }],
|
|
341
|
+
mutant: { ...mutant, artifact },
|
|
342
|
+
});
|
|
343
|
+
const result = await docsJourneyHandler(c);
|
|
344
|
+
expect(result.status).toBe(400);
|
|
345
|
+
expect(result.data.error).toContain('"mutant.artifact"');
|
|
346
|
+
expect(mockRunJourney).not.toHaveBeenCalled();
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
test.each([
|
|
351
|
+
[{ operator: 'drop-everything' }, '"mutant.operator"'],
|
|
352
|
+
[{ key: 5 }, '"mutant.key"'],
|
|
353
|
+
])('docsJourneyHandler refuses a bad mutant %j with 400', async (override, field) => {
|
|
354
|
+
const c = createContext({
|
|
355
|
+
pageId: 'form',
|
|
356
|
+
steps: [{ click: 'submit' }],
|
|
357
|
+
mutant: { ...mutant, ...override },
|
|
358
|
+
});
|
|
359
|
+
const result = await docsJourneyHandler(c);
|
|
360
|
+
expect(result.status).toBe(400);
|
|
361
|
+
expect(result.data.error).toContain(field);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('docsJourneyHandler passes no mutant cookie and returns no mutant without a mutant', async () => {
|
|
365
|
+
const c = createContext({ pageId: 'form', steps: [{ click: 'submit' }] });
|
|
366
|
+
const result = await docsJourneyHandler(c);
|
|
367
|
+
expect(mockRunJourney.mock.calls[0][0].mutantCookie).toBeUndefined();
|
|
368
|
+
expect(result.data.mutant).toBeUndefined();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('docsJourneyHandler passes a recording through as a test run of source journey', async () => {
|
|
372
|
+
const c = createContext({
|
|
373
|
+
pageId: 'form',
|
|
374
|
+
steps: [{ click: 'submit' }],
|
|
375
|
+
recording: { run: '20261003T151200Z-p0d4rm', journey: 'tests/journeys/a.yaml#Assign' },
|
|
376
|
+
});
|
|
377
|
+
await docsJourneyHandler(c);
|
|
378
|
+
expect(mockRunJourney).toHaveBeenCalledWith(
|
|
379
|
+
expect.objectContaining({
|
|
380
|
+
recording: {
|
|
381
|
+
source: 'journey',
|
|
382
|
+
run: { id: '20261003T151200Z-p0d4rm', by: 'test', journey: 'tests/journeys/a.yaml#Assign' },
|
|
383
|
+
},
|
|
384
|
+
})
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('docsJourneyHandler returns 400 for a malformed recording', async () => {
|
|
389
|
+
for (const recording of [
|
|
390
|
+
{ run: '../x' },
|
|
391
|
+
{ run: '' },
|
|
392
|
+
'run',
|
|
393
|
+
{ run: '20261003T151200Z-p0d4rm', journey: 7 },
|
|
394
|
+
]) {
|
|
395
|
+
const result = await docsJourneyHandler(
|
|
396
|
+
createContext({ pageId: 'form', steps: [{ click: 'submit' }], recording })
|
|
397
|
+
);
|
|
398
|
+
expect(result.status).toBe(400);
|
|
399
|
+
}
|
|
400
|
+
expect(mockRunJourney).not.toHaveBeenCalled();
|
|
401
|
+
});
|
|
@@ -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 { jest } from '@jest/globals';
|
|
18
|
+
|
|
19
|
+
// The real runner refuses these journeys before it fetches a browser; the mock
|
|
20
|
+
// proves it never gets that far.
|
|
21
|
+
const mockGetBrowser = jest.fn();
|
|
22
|
+
jest.unstable_mockModule('../../../lib/docs/getBrowser.js', () => ({
|
|
23
|
+
getBrowser: mockGetBrowser,
|
|
24
|
+
openPage: jest.fn(),
|
|
25
|
+
buildPageUrl: jest.fn(),
|
|
26
|
+
}));
|
|
27
|
+
jest.unstable_mockModule('../../../lib/build/config.js', () => ({ default: { basePath: '' } }));
|
|
28
|
+
jest.unstable_mockModule('../../../lib/docs/getBuildId.js', () => ({
|
|
29
|
+
default: jest.fn(() => 'build-1'),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const { default: docsJourneyHandler } = await import('./journey.js');
|
|
33
|
+
|
|
34
|
+
function createContext(body) {
|
|
35
|
+
const request = new Request('http://localhost:3227/lowdefy-docs/journey', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'content-type': 'application/json' },
|
|
38
|
+
body: JSON.stringify(body),
|
|
39
|
+
});
|
|
40
|
+
const json = jest.fn((data, status) => ({ data, status: status ?? 200 }));
|
|
41
|
+
return { req: { url: request.url, json: () => request.json() }, json };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test('POST /lowdefy-docs/journey answers 400 for a placeholder value (from: shape)', async () => {
|
|
45
|
+
const result = await docsJourneyHandler(
|
|
46
|
+
createContext({
|
|
47
|
+
pageId: 'form',
|
|
48
|
+
steps: [{ fill: { blockId: 'title', value: null, from: 'shape' } }],
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
expect(result.status).toBe(400);
|
|
52
|
+
expect(result.data.error).toMatch('has a placeholder value (from: shape)');
|
|
53
|
+
expect(mockGetBrowser).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('POST /lowdefy-docs/journey answers 400 for an email step on a server with no mail sink', async () => {
|
|
57
|
+
const previous = process.env.LOWDEFY_SERVER_DEV_MAIL_SINK;
|
|
58
|
+
delete process.env.LOWDEFY_SERVER_DEV_MAIL_SINK;
|
|
59
|
+
try {
|
|
60
|
+
const result = await docsJourneyHandler(
|
|
61
|
+
createContext({ pageId: 'form', steps: [{ email: { to: 'ada@example.test' } }] })
|
|
62
|
+
);
|
|
63
|
+
expect(result.status).toBe(400);
|
|
64
|
+
expect(result.data.error).toMatch('this dev server captures no mail');
|
|
65
|
+
expect(mockGetBrowser).not.toHaveBeenCalled();
|
|
66
|
+
} finally {
|
|
67
|
+
if (previous !== undefined) {
|
|
68
|
+
process.env.LOWDEFY_SERVER_DEV_MAIL_SINK = previous;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { parseRequestBody } from '@lowdefy/api';
|
|
18
|
+
|
|
17
19
|
import loadState from '../../../lib/docs/loadState.js';
|
|
18
20
|
import parseUserParam from './parseUserParam.js';
|
|
19
21
|
|
|
@@ -23,7 +25,7 @@ import parseUserParam from './parseUserParam.js';
|
|
|
23
25
|
// devMockRegistry and returns a URL for a human to open in a real browser
|
|
24
26
|
// tab (client/Inspector.jsx does the state injection there).
|
|
25
27
|
async function docsLoadStateHandler(c) {
|
|
26
|
-
const body = await c.req.
|
|
28
|
+
const body = parseRequestBody({ text: await c.req.text() });
|
|
27
29
|
const { name, mode } = body;
|
|
28
30
|
// Derived from the incoming request rather than a config value — this is
|
|
29
31
|
// the origin an agent can actually reach the dev server on (host/port it
|
|
@@ -35,7 +37,7 @@ async function docsLoadStateHandler(c) {
|
|
|
35
37
|
return c.json({ error: userError }, 400);
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
const result = await loadState({ origin, name, mode, user });
|
|
40
|
+
const result = await loadState({ origin, name, mode, user, data: body.data });
|
|
39
41
|
if (result.error) {
|
|
40
42
|
// A contradictory call (`user` in 'registry-only' mode) is the caller's
|
|
41
43
|
// mistake, not a failed render — 502 would read as "the renderer broke" and
|
package/src/routes/docs/mcp.js
CHANGED
|
@@ -16,15 +16,46 @@
|
|
|
16
16
|
|
|
17
17
|
import { StreamableHTTPTransport } from '@hono/mcp';
|
|
18
18
|
|
|
19
|
-
import createDocsMcpServer
|
|
19
|
+
import createDocsMcpServer, {
|
|
20
|
+
subscribeMcpServerToDevEvents,
|
|
21
|
+
} from '../../../lib/docs/createDocsMcpServer.js';
|
|
22
|
+
import { bootedAt } from '../../../lib/docs/devEventBus.js';
|
|
23
|
+
import keepEventStreamAlive from '../../../lib/docs/keepEventStreamAlive.js';
|
|
20
24
|
|
|
21
25
|
// Stateless per-request server: docs tools read build artifacts fresh on
|
|
22
26
|
// every call, so there is no session state worth keeping between requests.
|
|
27
|
+
//
|
|
28
|
+
// A GET opens the transport's standalone SSE stream, which is where server →
|
|
29
|
+
// client notifications go, so only that request subscribes to dev events. The
|
|
30
|
+
// POST path stays stateless — no sessionIdGenerator, no session registry.
|
|
23
31
|
async function mcpHandler(c) {
|
|
24
|
-
const server = createDocsMcpServer({
|
|
32
|
+
const server = createDocsMcpServer({
|
|
33
|
+
origin: new URL(c.req.url).origin,
|
|
34
|
+
honoContext: c,
|
|
35
|
+
version: process.env.LOWDEFY_SERVER_DEV_VERSION,
|
|
36
|
+
});
|
|
25
37
|
const transport = new StreamableHTTPTransport();
|
|
26
38
|
await server.connect(transport);
|
|
27
|
-
|
|
39
|
+
if (c.req.method !== 'GET') {
|
|
40
|
+
return keepEventStreamAlive({ response: await transport.handleRequest(c) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Three unsubscribe paths: an explicit close, a dropped connection, and (via
|
|
44
|
+
// the bus pruning a send that rejects) a stream gone without either firing.
|
|
45
|
+
const unsubscribe = subscribeMcpServerToDevEvents(server);
|
|
46
|
+
server.server.onclose = unsubscribe;
|
|
47
|
+
c.req.raw.signal?.addEventListener('abort', unsubscribe, { once: true });
|
|
48
|
+
|
|
49
|
+
// handleRequest registers the standalone stream synchronously inside
|
|
50
|
+
// streamSSE, so the restart notice must be sent after it — before, the
|
|
51
|
+
// transport has no stream to write to and drops the message silently.
|
|
52
|
+
const response = await transport.handleRequest(c);
|
|
53
|
+
await server.server.sendLoggingMessage({
|
|
54
|
+
level: 'info',
|
|
55
|
+
logger: 'lowdefy',
|
|
56
|
+
data: { type: 'restart', timestamp: new Date().toISOString(), bootedAt },
|
|
57
|
+
});
|
|
58
|
+
return response;
|
|
28
59
|
}
|
|
29
60
|
|
|
30
61
|
export default mcpHandler;
|
|
@@ -0,0 +1,130 @@
|
|
|
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 { Hono } from 'hono';
|
|
18
|
+
import { jest } from '@jest/globals';
|
|
19
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
20
|
+
|
|
21
|
+
// A real McpServer with the logging capability rides the real @hono/mcp
|
|
22
|
+
// transport, so the test covers the actual notification path; only the docs
|
|
23
|
+
// tool registrations and the bus subscription are stubbed out.
|
|
24
|
+
const mockSubscribeMcpServerToDevEvents = jest.fn();
|
|
25
|
+
const mockUnsubscribe = jest.fn();
|
|
26
|
+
jest.unstable_mockModule('../../../lib/docs/createDocsMcpServer.js', () => ({
|
|
27
|
+
default: () =>
|
|
28
|
+
new McpServer(
|
|
29
|
+
{ name: 'lowdefy-docs-test', version: '1.0.0' },
|
|
30
|
+
{ capabilities: { logging: {} } }
|
|
31
|
+
),
|
|
32
|
+
subscribeMcpServerToDevEvents: mockSubscribeMcpServerToDevEvents,
|
|
33
|
+
}));
|
|
34
|
+
jest.unstable_mockModule('../../../lib/docs/devEventBus.js', () => ({
|
|
35
|
+
bootedAt: '2026-05-06T07:08:09.000Z',
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
const { default: docsMcpHandler } = await import('./mcp.js');
|
|
39
|
+
|
|
40
|
+
function createApp() {
|
|
41
|
+
const app = new Hono();
|
|
42
|
+
app.all('/lowdefy-docs/mcp', docsMcpHandler);
|
|
43
|
+
return app;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readFirstFrame(response) {
|
|
47
|
+
const reader = response.body.getReader();
|
|
48
|
+
const decoder = new TextDecoder();
|
|
49
|
+
let buffer = '';
|
|
50
|
+
while (!buffer.includes('\n\n')) {
|
|
51
|
+
const { value, done } = await reader.read();
|
|
52
|
+
if (done) throw new Error('Stream closed before a full frame arrived.');
|
|
53
|
+
buffer += decoder.decode(value, { stream: true });
|
|
54
|
+
}
|
|
55
|
+
const frame = buffer.slice(0, buffer.indexOf('\n\n'));
|
|
56
|
+
const dataLine = frame.split('\n').find((line) => line.startsWith('data:'));
|
|
57
|
+
return JSON.parse(dataLine.slice('data:'.length));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
mockSubscribeMcpServerToDevEvents.mockReturnValue(mockUnsubscribe);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('GET /lowdefy-docs/mcp subscribes to dev events and pushes the restart notification first', async () => {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const res = await createApp().request('/lowdefy-docs/mcp', {
|
|
67
|
+
headers: { accept: 'text/event-stream' },
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(res.status).toBe(200);
|
|
72
|
+
expect(res.headers.get('content-type')).toContain('text/event-stream');
|
|
73
|
+
expect(mockSubscribeMcpServerToDevEvents).toHaveBeenCalledTimes(1);
|
|
74
|
+
const [server] = mockSubscribeMcpServerToDevEvents.mock.calls[0];
|
|
75
|
+
expect(server).toBeInstanceOf(McpServer);
|
|
76
|
+
|
|
77
|
+
const message = await readFirstFrame(res);
|
|
78
|
+
expect(message).toEqual({
|
|
79
|
+
jsonrpc: '2.0',
|
|
80
|
+
method: 'notifications/message',
|
|
81
|
+
params: {
|
|
82
|
+
level: 'info',
|
|
83
|
+
logger: 'lowdefy',
|
|
84
|
+
data: {
|
|
85
|
+
type: 'restart',
|
|
86
|
+
timestamp: expect.any(String),
|
|
87
|
+
bootedAt: '2026-05-06T07:08:09.000Z',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
controller.abort();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('aborting the GET stream unsubscribes from dev events', async () => {
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
const res = await createApp().request('/lowdefy-docs/mcp', {
|
|
97
|
+
headers: { accept: 'text/event-stream' },
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
});
|
|
100
|
+
await readFirstFrame(res);
|
|
101
|
+
expect(mockUnsubscribe).not.toHaveBeenCalled();
|
|
102
|
+
|
|
103
|
+
controller.abort();
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
105
|
+
|
|
106
|
+
expect(mockUnsubscribe).toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('POST /lowdefy-docs/mcp stays stateless and does not subscribe to dev events', async () => {
|
|
110
|
+
const res = await createApp().request('/lowdefy-docs/mcp', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { accept: 'application/json, text/event-stream', 'content-type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
jsonrpc: '2.0',
|
|
115
|
+
id: 1,
|
|
116
|
+
method: 'initialize',
|
|
117
|
+
params: {
|
|
118
|
+
protocolVersion: '2025-03-26',
|
|
119
|
+
capabilities: {},
|
|
120
|
+
clientInfo: { name: 'test-client', version: '1.0.0' },
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(res.status).toBe(200);
|
|
126
|
+
expect(res.headers.get('mcp-session-id')).toBeNull();
|
|
127
|
+
expect(mockSubscribeMcpServerToDevEvents).not.toHaveBeenCalled();
|
|
128
|
+
const message = await readFirstFrame(res);
|
|
129
|
+
expect(message.result.capabilities).toEqual({ logging: {} });
|
|
130
|
+
});
|