@lowdefy/server-dev 6.1.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +78 -0
- package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
- package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +727 -0
- package/lib/docs/dataSets/describeDataSetResult.js +25 -0
- package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
- package/lib/docs/dataSets/describeTargetKey.js +29 -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/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 +52 -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 +133 -0
- package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +493 -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 +83 -0
- package/lib/docs/dataSets/readDataSet.test.mjs +251 -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/sessionDatabaseName.js +32 -0
- package/lib/docs/dataSets/sweepOrphanDatabases.js +36 -0
- package/lib/docs/dataSets/test/app/lowdefy.yaml +225 -0
- package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
- package/lib/docs/dataSets/test/app/tests/data/shop.yaml +12 -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 +81 -7
- package/lib/docs/getBuildStatus.test.mjs +229 -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 +92 -0
- package/lib/server/applyDataSetRedirect.test.mjs +446 -0
- package/lib/server/applyDataSetRedirectTenantReach.test.mjs +207 -0
- package/lib/server/applyDataSetRedirectWalled.test.mjs +134 -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 +331 -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 +115 -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 +25 -3
- package/manager/processes/initialBuild.mjs +17 -1
- package/manager/processes/initialBuild.test.mjs +104 -0
- package/manager/processes/lowdefyBuild.mjs +31 -7
- package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +72 -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,326 @@
|
|
|
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 { EventEmitter } from 'node:events';
|
|
18
|
+
|
|
19
|
+
import { jest } from '@jest/globals';
|
|
20
|
+
|
|
21
|
+
const mockSpawn = jest.fn();
|
|
22
|
+
const { spawnSync, execSync, exec, execFile, fork } = await import('child_process');
|
|
23
|
+
jest.unstable_mockModule('child_process', () => ({
|
|
24
|
+
spawn: mockSpawn,
|
|
25
|
+
spawnSync,
|
|
26
|
+
execSync,
|
|
27
|
+
exec,
|
|
28
|
+
execFile,
|
|
29
|
+
fork,
|
|
30
|
+
}));
|
|
31
|
+
jest.unstable_mockModule('../utils/readBasePath.mjs', () => ({ default: () => '' }));
|
|
32
|
+
const mockWaitForServer = jest.fn(async () => true);
|
|
33
|
+
jest.unstable_mockModule('../utils/waitForServer.mjs', () => ({ default: mockWaitForServer }));
|
|
34
|
+
|
|
35
|
+
const { default: optimizeDependencies } = await import('./optimizeDependencies.mjs');
|
|
36
|
+
const { default: restartServer } = await import('./restartServer.mjs');
|
|
37
|
+
const { default: startFirstServer } = await import('./startFirstServer.mjs');
|
|
38
|
+
const { default: startServer } = await import('./startServer.mjs');
|
|
39
|
+
const { default: syncServer } = await import('./syncServer.mjs');
|
|
40
|
+
|
|
41
|
+
function createProcess() {
|
|
42
|
+
return Object.assign(new EventEmitter(), {
|
|
43
|
+
stdout: new EventEmitter(),
|
|
44
|
+
stderr: new EventEmitter(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createContext() {
|
|
49
|
+
const context = {
|
|
50
|
+
bin: { vite: '/server/node_modules/vite/bin/vite.js' },
|
|
51
|
+
directories: { config: '/apps/tenant', server: '/server' },
|
|
52
|
+
instance: { update: jest.fn() },
|
|
53
|
+
internalPort: 3211,
|
|
54
|
+
logger: { debug: jest.fn(), error: jest.fn(), info: jest.fn(), warn: jest.fn() },
|
|
55
|
+
mailSink: null,
|
|
56
|
+
markServerReady: jest.fn(),
|
|
57
|
+
options: { port: 3210 },
|
|
58
|
+
serverArtifacts: { record: jest.fn() },
|
|
59
|
+
shutdownServer: jest.fn(),
|
|
60
|
+
};
|
|
61
|
+
context.optimizeDependencies = optimizeDependencies(context);
|
|
62
|
+
context.syncServer = syncServer(context);
|
|
63
|
+
return context;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Like createServerArtifactTracker over a set of file contents. Its baseline
|
|
67
|
+
// is taken when the context is made, before the initial build, so before the
|
|
68
|
+
// first record every artifact reads as changed.
|
|
69
|
+
function createTracker(files) {
|
|
70
|
+
let started = {};
|
|
71
|
+
return {
|
|
72
|
+
record: jest.fn(() => {
|
|
73
|
+
started = { ...files };
|
|
74
|
+
}),
|
|
75
|
+
check: jest.fn(() => {
|
|
76
|
+
const changed = Object.keys(files).filter((file) => files[file] !== started[file]);
|
|
77
|
+
return { install: changed.includes('package.json'), restart: changed.length > 0 };
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function flush() {
|
|
83
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let processes;
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
processes = [];
|
|
90
|
+
mockWaitForServer.mockReset();
|
|
91
|
+
mockWaitForServer.mockImplementation(async () => true);
|
|
92
|
+
mockSpawn.mockImplementation(() => {
|
|
93
|
+
const child = createProcess();
|
|
94
|
+
processes.push(child);
|
|
95
|
+
return child;
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('startFirstServer runs the optimiser and waits for it to exit before it spawns the child', async () => {
|
|
100
|
+
const context = createContext();
|
|
101
|
+
|
|
102
|
+
const started = startFirstServer(context);
|
|
103
|
+
await flush();
|
|
104
|
+
|
|
105
|
+
expect(mockSpawn).toHaveBeenCalledTimes(1);
|
|
106
|
+
expect(mockSpawn.mock.calls[0][0]).toBe('node');
|
|
107
|
+
expect(mockSpawn.mock.calls[0][1]).toEqual(['/server/node_modules/vite/bin/vite.js', 'optimize']);
|
|
108
|
+
expect(mockSpawn.mock.calls[0][2].cwd).toBe('/server');
|
|
109
|
+
|
|
110
|
+
processes[0].emit('exit', 0);
|
|
111
|
+
await started;
|
|
112
|
+
|
|
113
|
+
expect(mockSpawn).toHaveBeenCalledTimes(2);
|
|
114
|
+
expect(mockSpawn.mock.calls[1][1].slice(0, 3)).toEqual([
|
|
115
|
+
'--expose-gc',
|
|
116
|
+
'/server/node_modules/vite/bin/vite.js',
|
|
117
|
+
'--host',
|
|
118
|
+
]);
|
|
119
|
+
expect(context.logger.warn).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('startFirstServer still starts the child when the optimiser fails, and warns', async () => {
|
|
123
|
+
const context = createContext();
|
|
124
|
+
|
|
125
|
+
const started = startFirstServer(context);
|
|
126
|
+
await flush();
|
|
127
|
+
processes[0].stderr.emit('data', Buffer.from('error when optimizing deps:\nboom\n'));
|
|
128
|
+
processes[0].stderr.emit('end');
|
|
129
|
+
processes[0].emit('exit', 1);
|
|
130
|
+
await started;
|
|
131
|
+
|
|
132
|
+
expect(mockSpawn).toHaveBeenCalledTimes(2);
|
|
133
|
+
expect(context.logger.warn).toHaveBeenCalledTimes(1);
|
|
134
|
+
expect(context.logger.warn.mock.calls[0][0]).toContain('boom');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('the optimiser resolves the same environment as the child, apart from the browser tag and stdin owner watch', async () => {
|
|
138
|
+
process.env.BETTER_AUTH_URL = 'http://localhost:3000';
|
|
139
|
+
try {
|
|
140
|
+
const context = createContext();
|
|
141
|
+
context.mailSink = {};
|
|
142
|
+
|
|
143
|
+
const started = startFirstServer(context);
|
|
144
|
+
await flush();
|
|
145
|
+
processes[0].emit('exit', 0);
|
|
146
|
+
await started;
|
|
147
|
+
|
|
148
|
+
const optimiserEnv = mockSpawn.mock.calls[0][2].env;
|
|
149
|
+
// The child alone has its stdin held by the manager, so only it exits when
|
|
150
|
+
// that pipe closes.
|
|
151
|
+
const { LOWDEFY_BROWSER_TAG, LOWDEFY_EXIT_ON_STDIN_CLOSE, ...childEnv } =
|
|
152
|
+
mockSpawn.mock.calls[1][2].env;
|
|
153
|
+
expect(LOWDEFY_BROWSER_TAG).toBeDefined();
|
|
154
|
+
expect(LOWDEFY_EXIT_ON_STDIN_CLOSE).toBe('1');
|
|
155
|
+
expect(optimiserEnv).toEqual(childEnv);
|
|
156
|
+
expect(optimiserEnv.BETTER_AUTH_URL).toBe('http://localhost:3210');
|
|
157
|
+
expect(optimiserEnv.LOWDEFY_SERVER_DEV_MAIL_SINK).toBe('true');
|
|
158
|
+
expect(optimiserEnv.LOWDEFY_DIRECTORY_CONFIG).toBe('/apps/tenant');
|
|
159
|
+
} finally {
|
|
160
|
+
delete process.env.BETTER_AUTH_URL;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('a watcher sync during the optimise waits for the first start and restarts nothing', async () => {
|
|
165
|
+
const context = createContext();
|
|
166
|
+
context.serverArtifacts = createTracker({ 'build/config.json': 'a', 'package.json': 'a' });
|
|
167
|
+
context.restartServer = jest.fn(async () => startServer(context));
|
|
168
|
+
|
|
169
|
+
const started = startFirstServer(context);
|
|
170
|
+
await flush();
|
|
171
|
+
// A late file event from the initial build: its batch syncs during the optimise.
|
|
172
|
+
let synced = false;
|
|
173
|
+
const sync = context.syncServer().then(() => {
|
|
174
|
+
synced = true;
|
|
175
|
+
});
|
|
176
|
+
await flush();
|
|
177
|
+
expect(synced).toBe(false);
|
|
178
|
+
|
|
179
|
+
processes[0].emit('exit', 0);
|
|
180
|
+
await Promise.all([started, sync]);
|
|
181
|
+
|
|
182
|
+
expect(synced).toBe(true);
|
|
183
|
+
expect(context.restartServer).not.toHaveBeenCalled();
|
|
184
|
+
// The optimiser and one child.
|
|
185
|
+
expect(mockSpawn).toHaveBeenCalledTimes(2);
|
|
186
|
+
expect(mockSpawn.mock.calls[1][1]).toContain('--host');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('a config edit during the optimise is rebuilt at once, and its sync resolves after the first start', async () => {
|
|
190
|
+
const context = createContext();
|
|
191
|
+
const files = { 'build/config.json': 'a', 'package.json': 'a' };
|
|
192
|
+
context.serverArtifacts = createTracker(files);
|
|
193
|
+
context.lowdefyBuild = jest.fn(async () => {
|
|
194
|
+
files['build/config.json'] = 'b';
|
|
195
|
+
});
|
|
196
|
+
context.restartServer = jest.fn(async () => {
|
|
197
|
+
context.serverArtifacts.record();
|
|
198
|
+
startServer(context);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const started = startFirstServer(context);
|
|
202
|
+
await flush();
|
|
203
|
+
// The lowdefyBuildWatcher batch for the edit.
|
|
204
|
+
let childrenAtSync;
|
|
205
|
+
const batch = (async () => {
|
|
206
|
+
await context.lowdefyBuild();
|
|
207
|
+
await context.syncServer();
|
|
208
|
+
childrenAtSync = mockSpawn.mock.calls.length;
|
|
209
|
+
})();
|
|
210
|
+
await flush();
|
|
211
|
+
expect(context.lowdefyBuild).toHaveBeenCalledTimes(1);
|
|
212
|
+
expect(childrenAtSync).toBeUndefined();
|
|
213
|
+
|
|
214
|
+
processes[0].emit('exit', 0);
|
|
215
|
+
await Promise.all([started, batch]);
|
|
216
|
+
|
|
217
|
+
// The child may have read the edited config at spawn; one restart covers the window.
|
|
218
|
+
expect(context.restartServer).toHaveBeenCalledTimes(1);
|
|
219
|
+
expect(childrenAtSync).toBe(3);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('a plugin package added during the optimise is installed by the queued sync', async () => {
|
|
223
|
+
const context = createContext();
|
|
224
|
+
const files = { 'build/config.json': 'a', 'package.json': 'a' };
|
|
225
|
+
context.serverArtifacts = createTracker(files);
|
|
226
|
+
const events = [];
|
|
227
|
+
context.buildActivity = { track: (task) => task() };
|
|
228
|
+
context.installPlugins = jest.fn(async () => events.push('install'));
|
|
229
|
+
context.lowdefyBuild = jest.fn(async () => events.push('build'));
|
|
230
|
+
context.restartServer = jest.fn(async () => events.push('restart'));
|
|
231
|
+
let finishOptimise;
|
|
232
|
+
context.optimizeDependencies = jest.fn(async () => events.push('optimise'));
|
|
233
|
+
context.optimizeDependencies.mockImplementationOnce(
|
|
234
|
+
() =>
|
|
235
|
+
new Promise((resolve) => {
|
|
236
|
+
events.push('first optimise');
|
|
237
|
+
finishOptimise = resolve;
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
context.syncServer = syncServer(context);
|
|
241
|
+
|
|
242
|
+
const started = startFirstServer(context);
|
|
243
|
+
await flush();
|
|
244
|
+
// A config build during the optimise adds a plugin package to package.json.
|
|
245
|
+
files['package.json'] = 'b';
|
|
246
|
+
const sync = context.syncServer();
|
|
247
|
+
finishOptimise();
|
|
248
|
+
await Promise.all([started, sync]);
|
|
249
|
+
|
|
250
|
+
// The first child spawns with the new package.json, but it was never
|
|
251
|
+
// installed: the baseline is the one from before the optimise.
|
|
252
|
+
expect(mockSpawn).toHaveBeenCalledTimes(1);
|
|
253
|
+
expect(events).toEqual(['first optimise', 'install', 'build', 'optimise', 'restart']);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('startFirstServer resolves once the first child answers, before a queued sync restarts it', async () => {
|
|
257
|
+
const context = createContext();
|
|
258
|
+
const files = { 'build/config.json': 'a', 'package.json': 'a' };
|
|
259
|
+
context.serverArtifacts = createTracker(files);
|
|
260
|
+
context.restartServer = jest.fn(async () => {
|
|
261
|
+
context.serverArtifacts.record();
|
|
262
|
+
startServer(context);
|
|
263
|
+
});
|
|
264
|
+
let answer;
|
|
265
|
+
mockWaitForServer.mockImplementationOnce(
|
|
266
|
+
() =>
|
|
267
|
+
new Promise((resolve) => {
|
|
268
|
+
answer = resolve;
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const started = startFirstServer(context);
|
|
273
|
+
await flush();
|
|
274
|
+
// A config edit built during the optimise: its sync restarts the child.
|
|
275
|
+
files['build/config.json'] = 'b';
|
|
276
|
+
const sync = context.syncServer();
|
|
277
|
+
processes[0].emit('exit', 0);
|
|
278
|
+
await flush();
|
|
279
|
+
|
|
280
|
+
// The first child is waited on, and nothing restarts it before it answers,
|
|
281
|
+
// or the manager would never record the server as ready.
|
|
282
|
+
expect(mockWaitForServer).toHaveBeenCalledTimes(1);
|
|
283
|
+
expect(mockWaitForServer.mock.calls[0][0].child).toBe(processes[1]);
|
|
284
|
+
expect(context.restartServer).not.toHaveBeenCalled();
|
|
285
|
+
|
|
286
|
+
answer(true);
|
|
287
|
+
expect(await started).toBe(true);
|
|
288
|
+
await sync;
|
|
289
|
+
expect(context.restartServer).toHaveBeenCalledTimes(1);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('startFirstServer marks the server ready when the first child answers', async () => {
|
|
293
|
+
const context = createContext();
|
|
294
|
+
|
|
295
|
+
const started = startFirstServer(context);
|
|
296
|
+
await flush();
|
|
297
|
+
processes[0].emit('exit', 0);
|
|
298
|
+
|
|
299
|
+
expect(await started).toBe(true);
|
|
300
|
+
expect(context.markServerReady).toHaveBeenCalledTimes(1);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('a restart marks the server ready when the first child exited without answering', async () => {
|
|
304
|
+
const context = createContext();
|
|
305
|
+
context.buildActivity = { track: (task) => task() };
|
|
306
|
+
context.restartServer = restartServer(context);
|
|
307
|
+
context.serverArtifacts = createTracker({ 'build/config.json': 'a', 'package.json': 'a' });
|
|
308
|
+
// The first child exits at start (a plugin that fails to load): neither
|
|
309
|
+
// its wait nor the later one sees an answer. The restarted child answers.
|
|
310
|
+
mockWaitForServer.mockImplementationOnce(async () => false);
|
|
311
|
+
mockWaitForServer.mockImplementationOnce(async () => false);
|
|
312
|
+
|
|
313
|
+
const started = startFirstServer(context);
|
|
314
|
+
await flush();
|
|
315
|
+
processes[0].emit('exit', 0);
|
|
316
|
+
expect(await started).toBe(false);
|
|
317
|
+
await flush();
|
|
318
|
+
expect(context.markServerReady).not.toHaveBeenCalled();
|
|
319
|
+
|
|
320
|
+
// The fix to the plugin is a server-side plugin edit, which restarts.
|
|
321
|
+
await context.syncServer({ restart: true });
|
|
322
|
+
|
|
323
|
+
expect(mockSpawn).toHaveBeenCalledTimes(3);
|
|
324
|
+
expect(mockWaitForServer.mock.calls[2][0].child).toBe(processes[2]);
|
|
325
|
+
expect(context.markServerReady).toHaveBeenCalledTimes(1);
|
|
326
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
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 getMailOutboxDirectory from '../../lib/docs/getMailOutboxDirectory.js';
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
With LOWDEFY_DEV_SMTP_PORT set, the dev server receives mail over SMTP on that
|
|
24
|
+
loopback port and keeps it instead of delivering it: each message becomes one
|
|
25
|
+
JSON file in <app>/.lowdefy/mail/, which journeys (the `email` step), agents and
|
|
26
|
+
people read. The app sends through its own SMTP connection, pointed at this
|
|
27
|
+
port by its own secrets, so the whole send path - connection config, delivery
|
|
28
|
+
filter, nodemailer, templates - runs exactly as in production. The manager owns
|
|
29
|
+
the listener so it survives restarts of the server child, and tells each child
|
|
30
|
+
it is listening (startServer passes LOWDEFY_SERVER_DEV_MAIL_SINK), so a port
|
|
31
|
+
added to .env after start is not mistaken for a running sink. The outbox is
|
|
32
|
+
cleared at start, so a test run only ever sees the mail it caused.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
function readSmtpPort() {
|
|
36
|
+
const value = process.env.LOWDEFY_DEV_SMTP_PORT;
|
|
37
|
+
if (value === undefined || value === '') {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const port = Number(value);
|
|
41
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
42
|
+
throw new Error(`LOWDEFY_DEV_SMTP_PORT must be a port number. Received "${value}".`);
|
|
43
|
+
}
|
|
44
|
+
return port;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readStream(stream) {
|
|
48
|
+
const chunks = [];
|
|
49
|
+
for await (const chunk of stream) {
|
|
50
|
+
chunks.push(chunk);
|
|
51
|
+
}
|
|
52
|
+
return Buffer.concat(chunks);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createMessageWriter({ directory, PostalMime }) {
|
|
56
|
+
let sequence = 0;
|
|
57
|
+
return async function writeMessage({ raw, envelope }) {
|
|
58
|
+
// Numbered on arrival, before the parse, so the outbox order is the order
|
|
59
|
+
// the app sent in even when two parses finish out of order.
|
|
60
|
+
sequence += 1;
|
|
61
|
+
const id = String(sequence).padStart(6, '0');
|
|
62
|
+
const parsed = await PostalMime.parse(raw);
|
|
63
|
+
const message = {
|
|
64
|
+
id,
|
|
65
|
+
receivedAt: new Date().toISOString(),
|
|
66
|
+
from: envelope.mailFrom ? envelope.mailFrom.address : null,
|
|
67
|
+
to: envelope.rcptTo.map(({ address }) => address),
|
|
68
|
+
subject: parsed.subject ?? '',
|
|
69
|
+
html: parsed.html ?? null,
|
|
70
|
+
text: parsed.text ?? null,
|
|
71
|
+
};
|
|
72
|
+
const filePath = path.join(directory, `${id}.json`);
|
|
73
|
+
await fs.promises.writeFile(`${filePath}.tmp`, JSON.stringify(message, null, 2));
|
|
74
|
+
await fs.promises.rename(`${filePath}.tmp`, filePath);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function listen({ server, port }) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
function onError(error) {
|
|
81
|
+
if (error.code === 'EADDRINUSE') {
|
|
82
|
+
reject(
|
|
83
|
+
new Error(
|
|
84
|
+
`LOWDEFY_DEV_SMTP_PORT ${port} is already in use. Stop the other process or choose another port.`
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
reject(error);
|
|
90
|
+
}
|
|
91
|
+
server.once('error', onError);
|
|
92
|
+
server.listen(port, '127.0.0.1', () => {
|
|
93
|
+
server.removeListener('error', onError);
|
|
94
|
+
resolve();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function startMailSink(context) {
|
|
100
|
+
const port = readSmtpPort();
|
|
101
|
+
if (port === undefined) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
// Loaded only when capture is on: a dev server that never receives mail
|
|
105
|
+
// never loads an SMTP server.
|
|
106
|
+
const [{ default: PostalMime }, { default: smtpServer }] = await Promise.all([
|
|
107
|
+
import('postal-mime'),
|
|
108
|
+
import('smtp-server'),
|
|
109
|
+
]);
|
|
110
|
+
const directory = getMailOutboxDirectory({ configDirectory: context.directories.config });
|
|
111
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
112
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
113
|
+
const writeMessage = createMessageWriter({ directory, PostalMime });
|
|
114
|
+
|
|
115
|
+
const server = new smtpServer.SMTPServer({
|
|
116
|
+
authOptional: true,
|
|
117
|
+
disabledCommands: ['AUTH', 'STARTTLS'],
|
|
118
|
+
logger: false,
|
|
119
|
+
onData(stream, session, callback) {
|
|
120
|
+
// The SMTP reply waits for the file, so by the time the app's send
|
|
121
|
+
// resolves the message is readable.
|
|
122
|
+
readStream(stream)
|
|
123
|
+
.then((raw) => writeMessage({ raw, envelope: session.envelope }))
|
|
124
|
+
.then(() => callback(), callback);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
await listen({ server, port });
|
|
128
|
+
server.on('error', (error) => context.logger.error(error));
|
|
129
|
+
context.logger.info(
|
|
130
|
+
`Capturing mail sent to SMTP 127.0.0.1:${port} in ${directory} instead of delivering it.`
|
|
131
|
+
);
|
|
132
|
+
return server;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default startMailSink;
|
|
@@ -0,0 +1,146 @@
|
|
|
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 net from 'node:net';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
import { jest } from '@jest/globals';
|
|
23
|
+
import nodemailer from 'nodemailer';
|
|
24
|
+
|
|
25
|
+
import readMailOutbox from '../../lib/docs/readMailOutbox.js';
|
|
26
|
+
|
|
27
|
+
const { default: startMailSink } = await import('./startMailSink.mjs');
|
|
28
|
+
|
|
29
|
+
function getFreePort() {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const server = net.createServer();
|
|
32
|
+
server.listen(0, '127.0.0.1', () => {
|
|
33
|
+
const { port } = server.address();
|
|
34
|
+
server.close(() => resolve(port));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let configDirectory;
|
|
40
|
+
let sink;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-mail-sink-'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(async () => {
|
|
47
|
+
if (sink) {
|
|
48
|
+
await new Promise((resolve) => sink.close(resolve));
|
|
49
|
+
}
|
|
50
|
+
sink = undefined;
|
|
51
|
+
delete process.env.LOWDEFY_DEV_SMTP_PORT;
|
|
52
|
+
fs.rmSync(configDirectory, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function createContext() {
|
|
56
|
+
return {
|
|
57
|
+
directories: { config: configDirectory },
|
|
58
|
+
logger: { info: jest.fn(), error: jest.fn() },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test('startMailSink keeps a message sent through nodemailer with its recipients, subject and links intact', async () => {
|
|
63
|
+
const port = await getFreePort();
|
|
64
|
+
process.env.LOWDEFY_DEV_SMTP_PORT = String(port);
|
|
65
|
+
const staleDirectory = path.join(configDirectory, '.lowdefy', 'mail');
|
|
66
|
+
fs.mkdirSync(staleDirectory, { recursive: true });
|
|
67
|
+
fs.writeFileSync(path.join(staleDirectory, '000001.json'), '{"subject":"previous run"}');
|
|
68
|
+
sink = await startMailSink(createContext());
|
|
69
|
+
|
|
70
|
+
// Long enough that quoted-printable folds it across lines, and full of `=`,
|
|
71
|
+
// which quoted-printable escapes: the stored link must still be the one sent.
|
|
72
|
+
const link = `http://localhost:3210/api/auth/verify-email?token=${'eyJhbGciOiJIUzI1NiJ9'.repeat(
|
|
73
|
+
8
|
|
74
|
+
)}&callbackURL=%2Fverify-email`;
|
|
75
|
+
const transport = nodemailer.createTransport({ host: '127.0.0.1', port, secure: false });
|
|
76
|
+
await transport.sendMail({
|
|
77
|
+
from: 'app@localhost',
|
|
78
|
+
to: 'Ada Lovelace <ada@example.test>',
|
|
79
|
+
bcc: 'audit@example.test',
|
|
80
|
+
subject: 'Verify your email address — café',
|
|
81
|
+
html: `<p>Hello</p><a href="${link}" target="_blank">Verify email address</a>`,
|
|
82
|
+
text: `Verify: ${link}`,
|
|
83
|
+
});
|
|
84
|
+
transport.close();
|
|
85
|
+
|
|
86
|
+
const messages = await readMailOutbox({ configDirectory });
|
|
87
|
+
expect(messages).toHaveLength(1);
|
|
88
|
+
expect(messages[0]).toMatchObject({
|
|
89
|
+
id: '000001',
|
|
90
|
+
from: 'app@localhost',
|
|
91
|
+
to: ['ada@example.test', 'audit@example.test'],
|
|
92
|
+
subject: 'Verify your email address — café',
|
|
93
|
+
});
|
|
94
|
+
expect(messages[0].text.trim()).toEqual(`Verify: ${link}`);
|
|
95
|
+
expect(messages[0].html).toContain(`href="${link}"`);
|
|
96
|
+
expect(Number.isNaN(Date.parse(messages[0].receivedAt))).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('startMailSink numbers messages in the order they arrive', async () => {
|
|
100
|
+
const port = await getFreePort();
|
|
101
|
+
process.env.LOWDEFY_DEV_SMTP_PORT = String(port);
|
|
102
|
+
sink = await startMailSink(createContext());
|
|
103
|
+
const transport = nodemailer.createTransport({ host: '127.0.0.1', port, secure: false });
|
|
104
|
+
await transport.sendMail({ from: 'app@localhost', to: 'a@example.test', subject: 'First' });
|
|
105
|
+
await transport.sendMail({ from: 'app@localhost', to: 'a@example.test', subject: 'Second' });
|
|
106
|
+
transport.close();
|
|
107
|
+
|
|
108
|
+
const messages = await readMailOutbox({ configDirectory });
|
|
109
|
+
expect(messages.map(({ id, subject }) => [id, subject])).toEqual([
|
|
110
|
+
['000001', 'First'],
|
|
111
|
+
['000002', 'Second'],
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('startMailSink starts nothing and leaves the outbox alone when LOWDEFY_DEV_SMTP_PORT is unset', async () => {
|
|
116
|
+
const outbox = path.join(configDirectory, '.lowdefy', 'mail');
|
|
117
|
+
fs.mkdirSync(outbox, { recursive: true });
|
|
118
|
+
fs.writeFileSync(path.join(outbox, '000001.json'), '{}');
|
|
119
|
+
expect(await startMailSink(createContext())).toBe(null);
|
|
120
|
+
expect(fs.readdirSync(outbox)).toEqual(['000001.json']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test.each(['smtp', '2525.5', '0', '70000'])(
|
|
124
|
+
'startMailSink rejects LOWDEFY_DEV_SMTP_PORT "%s"',
|
|
125
|
+
async (value) => {
|
|
126
|
+
process.env.LOWDEFY_DEV_SMTP_PORT = value;
|
|
127
|
+
await expect(startMailSink(createContext())).rejects.toThrow(
|
|
128
|
+
`LOWDEFY_DEV_SMTP_PORT must be a port number. Received "${value}".`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
test('startMailSink fails with the port when LOWDEFY_DEV_SMTP_PORT is taken', async () => {
|
|
134
|
+
const blocker = net.createServer();
|
|
135
|
+
const port = await new Promise((resolve) => {
|
|
136
|
+
blocker.listen(0, '127.0.0.1', () => resolve(blocker.address().port));
|
|
137
|
+
});
|
|
138
|
+
process.env.LOWDEFY_DEV_SMTP_PORT = String(port);
|
|
139
|
+
try {
|
|
140
|
+
await expect(startMailSink(createContext())).rejects.toThrow(
|
|
141
|
+
`LOWDEFY_DEV_SMTP_PORT ${port} is already in use.`
|
|
142
|
+
);
|
|
143
|
+
} finally {
|
|
144
|
+
await new Promise((resolve) => blocker.close(resolve));
|
|
145
|
+
}
|
|
146
|
+
});
|