@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.
Files changed (623) hide show
  1. package/client/App.jsx +52 -24
  2. package/client/Inspector.jsx +73 -30
  3. package/client/JourneyObserver.jsx +47 -0
  4. package/client/Page.jsx +54 -22
  5. package/client/Recorder.jsx +73 -0
  6. package/client/Routing.jsx +37 -13
  7. package/client/clientBuildImports.test.mjs +2 -0
  8. package/client/devStreamConnections.test.mjs +40 -0
  9. package/client/feedback/FeedbackMount.jsx +2 -2
  10. package/client/feedback/FeedbackOverlay.jsx +7 -4
  11. package/client/feedback/feedbackStyles.js +3 -0
  12. package/client/journeyObserver/startJourneyObserver.js +101 -0
  13. package/client/journeyObserver/startJourneyObserver.test.mjs +149 -0
  14. package/client/openInEditor/OpenInEditorListener.jsx +2 -0
  15. package/client/recorder/buildEngineRecord.js +37 -0
  16. package/client/recorder/buildInteractionRecord.js +64 -0
  17. package/client/recorder/buildTraceEvent.js +57 -0
  18. package/client/recorder/buildTraceEvent.test.mjs +81 -0
  19. package/client/recorder/chunkRecords.js +69 -0
  20. package/client/recorder/chunkRecords.test.mjs +59 -0
  21. package/client/recorder/collectEventActions.js +30 -0
  22. package/client/recorder/collectEventEndpoints.js +40 -0
  23. package/client/recorder/collectEventRequests.js +39 -0
  24. package/client/recorder/createPairingBuffer.js +187 -0
  25. package/client/recorder/createPairingBuffer.test.mjs +405 -0
  26. package/client/recorder/createPasswordRedactor.js +84 -0
  27. package/client/recorder/createRecordBatcher.js +54 -0
  28. package/client/recorder/createRecordBatcher.test.mjs +76 -0
  29. package/client/recorder/createRecorder.js +216 -0
  30. package/client/recorder/createRecorder.test.mjs +318 -0
  31. package/client/recorder/createRecordingSession.js +61 -0
  32. package/client/recorder/createRecordingSession.test.mjs +72 -0
  33. package/client/recorder/diffStateWrites.js +77 -0
  34. package/client/recorder/diffStateWrites.test.mjs +66 -0
  35. package/client/recorder/isEventCall.js +30 -0
  36. package/client/recorder/jsonType.js +39 -0
  37. package/client/recorder/keyChord.js +47 -0
  38. package/client/recorder/keyChord.test.mjs +50 -0
  39. package/client/recorder/sendRecords.js +40 -0
  40. package/client/recorder/toJsonSafe.js +29 -0
  41. package/lib/client/BuildingPage.jsx +3 -1
  42. package/lib/client/ErrorBar.jsx +17 -13
  43. package/lib/client/RedirectingPage.jsx +56 -0
  44. package/lib/client/auth/Auth.jsx +11 -6
  45. package/lib/client/auth/AuthConfigured.jsx +222 -36
  46. package/lib/client/auth/AuthNotConfigured.js +45 -4
  47. package/lib/client/auth/authNotConfiguredParity.test.mjs +61 -0
  48. package/lib/client/auth/authTemplateSync.test.mjs +40 -0
  49. package/lib/client/auth/betterAuthSessionContract.test.mjs +181 -0
  50. package/lib/client/auth/createFetchCheckingBuild.js +45 -0
  51. package/lib/client/bundleBuildId.js +25 -0
  52. package/lib/client/collectVisibleBlockIds.js +51 -0
  53. package/lib/client/getCspNonce.js +25 -0
  54. package/lib/client/getPagePath.js +36 -0
  55. package/lib/client/getPagePath.test.mjs +51 -0
  56. package/lib/client/utils/formatErrorsForCopy.js +30 -0
  57. package/lib/client/utils/formatErrorsForCopy.test.mjs +39 -0
  58. package/lib/client/utils/getErrorBarColor.js +25 -0
  59. package/lib/client/utils/getErrorBarColor.test.mjs +34 -0
  60. package/lib/client/utils/request.js +8 -1
  61. package/lib/client/utils/usePageConfig.js +89 -25
  62. package/lib/client/utils/usePageConfig.test.mjs +198 -13
  63. package/lib/configTree/buildConfigTree.mjs +58 -0
  64. package/lib/configTree/buildConfigTree.test.mjs +77 -0
  65. package/lib/configTree/collectConfigText.mjs +101 -0
  66. package/lib/configTree/collectConfigText.test.mjs +116 -0
  67. package/lib/configTree/prepareScratchServer.mjs +46 -0
  68. package/lib/configTree/runConfigTreeBuild.mjs +88 -0
  69. package/lib/configTree/runConfigTreeBuild.skipPluginChecks.test.mjs +74 -0
  70. package/lib/configTree/runConfigTreeBuild.test.mjs +202 -0
  71. package/lib/docs/JourneyStepError.js +30 -0
  72. package/lib/docs/acquireBrowserSlot.js +30 -0
  73. package/lib/docs/addNestedEndpoints.js +54 -0
  74. package/lib/docs/addNestedEndpoints.test.mjs +185 -0
  75. package/lib/docs/addWriteFlags.js +77 -0
  76. package/lib/docs/buildEditedPages.js +50 -0
  77. package/lib/docs/buildWaitHeader.js +21 -0
  78. package/lib/docs/captureAnnotatedScreenshot.js +23 -12
  79. package/lib/docs/checkAgentFiles.js +148 -0
  80. package/lib/docs/checkAgentFiles.test.mjs +225 -0
  81. package/lib/docs/checkStepReferences.js +239 -0
  82. package/lib/docs/checkStepReferences.test.mjs +173 -0
  83. package/lib/docs/checkpointStore.js +3 -0
  84. package/lib/docs/clientErrorStore.js +18 -1
  85. package/lib/docs/clientErrorStore.test.mjs +46 -0
  86. package/lib/docs/collectExercised.js +52 -0
  87. package/lib/docs/createBrowserLifecycle.js +102 -0
  88. package/lib/docs/createBrowserLifecycle.test.mjs +212 -0
  89. package/lib/docs/createDocsMcpServer.js +345 -388
  90. package/lib/docs/createDocsMcpServer.test.mjs +644 -4
  91. package/lib/docs/createDocsMcpServerRunJourney.test.mjs +148 -0
  92. package/lib/docs/createJourneyActors.chromium.test.mjs +227 -0
  93. package/lib/docs/createJourneyActors.js +213 -0
  94. package/lib/docs/createJourneyActors.test.mjs +79 -0
  95. package/lib/docs/createJourneyObservations.js +80 -0
  96. package/lib/docs/createLeftOriginError.js +26 -0
  97. package/lib/docs/createNetworkCounter.js +101 -0
  98. package/lib/docs/dataSets/checkSharedCollectionIds.js +67 -0
  99. package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
  100. package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +679 -0
  101. package/lib/docs/dataSets/describeDataSetResult.js +25 -0
  102. package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
  103. package/lib/docs/dataSets/deserializeFixture.js +34 -0
  104. package/lib/docs/dataSets/drainSessionWork.js +37 -0
  105. package/lib/docs/dataSets/drainSessionWork.test.mjs +64 -0
  106. package/lib/docs/dataSets/duplicateIndexName.js +24 -0
  107. package/lib/docs/dataSets/findCollectionCollisions.js +52 -0
  108. package/lib/docs/dataSets/getDataStore.js +106 -0
  109. package/lib/docs/dataSets/getDataStore.mongodb.test.mjs +63 -0
  110. package/lib/docs/dataSets/getDataStore.test.mjs +195 -0
  111. package/lib/docs/dataSets/groupDataSetByCollection.js +44 -0
  112. package/lib/docs/dataSets/insertFixture.js +57 -0
  113. package/lib/docs/dataSets/insertGeneratedDocuments.js +41 -0
  114. package/lib/docs/dataSets/isDataStoreGone.js +29 -0
  115. package/lib/docs/dataSets/loadDataSetCollection.js +42 -0
  116. package/lib/docs/dataSets/mergeCollectionIndexes.js +52 -0
  117. package/lib/docs/dataSets/mergeCollectionIndexes.test.mjs +59 -0
  118. package/lib/docs/dataSets/openDataSession.js +119 -0
  119. package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +343 -0
  120. package/lib/docs/dataSets/pickDataStorePort.js +58 -0
  121. package/lib/docs/dataSets/pickDataStorePort.test.mjs +63 -0
  122. package/lib/docs/dataSets/readConnectionArtifacts.js +46 -0
  123. package/lib/docs/dataSets/readDataSession.js +41 -0
  124. package/lib/docs/dataSets/readDataSession.test.mjs +57 -0
  125. package/lib/docs/dataSets/readDataSet.js +68 -0
  126. package/lib/docs/dataSets/readDataSet.test.mjs +220 -0
  127. package/lib/docs/dataSets/registerDataStoreShutdown.js +47 -0
  128. package/lib/docs/dataSets/removeDataStoreFiles.js +32 -0
  129. package/lib/docs/dataSets/resolveDataSetCollection.js +46 -0
  130. package/lib/docs/dataSets/resolveJourneyDataSet.js +106 -0
  131. package/lib/docs/dataSets/resolveJourneyDataSet.test.mjs +130 -0
  132. package/lib/docs/dataSets/sweepOrphanDatabases.js +33 -0
  133. package/lib/docs/dataSets/test/app/lowdefy.yaml +203 -0
  134. package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
  135. package/lib/docs/dataSets/test/app/tests/data/shop.yaml +8 -0
  136. package/lib/docs/dataSets/test/buildTestApp.mjs +43 -0
  137. package/lib/docs/devEventBus.js +136 -0
  138. package/lib/docs/devEventBus.test.mjs +269 -0
  139. package/lib/docs/devToolDefinitions.js +529 -0
  140. package/lib/docs/docSources.js +21 -0
  141. package/lib/docs/docs.test.mjs +171 -15
  142. package/lib/docs/docsIndex.test.mjs +377 -0
  143. package/lib/docs/evalOperator.js +5 -3
  144. package/lib/docs/evalOperatorHeadless.js +84 -12
  145. package/lib/docs/evalOperatorHeadless.test.mjs +7 -1
  146. package/lib/docs/evalOperatorInTab.js +2 -1
  147. package/lib/docs/findConfig.js +95 -49
  148. package/lib/docs/fitResponse.js +81 -0
  149. package/lib/docs/fitResponse.test.mjs +128 -0
  150. package/lib/docs/formatFeedback.js +26 -5
  151. package/lib/docs/formatFeedback.test.mjs +48 -0
  152. package/lib/docs/getAppMap.js +7 -1
  153. package/lib/docs/getBrowser.js +150 -80
  154. package/lib/docs/getBrowser.test.mjs +407 -16
  155. package/lib/docs/getBuildId.js +61 -0
  156. package/lib/docs/getBuildId.test.mjs +84 -0
  157. package/lib/docs/getBuildStatus.js +54 -6
  158. package/lib/docs/getBuildStatus.test.mjs +133 -0
  159. package/lib/docs/getBuildStatusAfterEdits.js +35 -0
  160. package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
  161. package/lib/docs/getCoreDoc.js +9 -1
  162. package/lib/docs/getDoc.js +100 -0
  163. package/lib/docs/getDocsIndex.js +152 -0
  164. package/lib/docs/getHazards.js +115 -0
  165. package/lib/docs/getHazards.test.mjs +175 -0
  166. package/lib/docs/getMailOutboxDirectory.js +26 -0
  167. package/lib/docs/getOverview.js +79 -13
  168. package/lib/docs/getOverviewBasePath.test.mjs +37 -0
  169. package/lib/docs/getPageBuildStatus.js +46 -0
  170. package/lib/docs/getPluginDoc.js +21 -23
  171. package/lib/docs/getSchema.js +33 -1
  172. package/lib/docs/getStaleStatus.js +36 -0
  173. package/lib/docs/getStaleStatus.test.mjs +74 -0
  174. package/lib/docs/guardJourneyOrigin.js +39 -0
  175. package/lib/docs/inspectState.js +5 -3
  176. package/lib/docs/inspectStateFromTab.js +2 -2
  177. package/lib/docs/inspectStateHeadless.js +89 -27
  178. package/lib/docs/inspectStateHeadless.test.mjs +20 -1
  179. package/lib/docs/installHeadlessShell.failure.test.mjs +45 -0
  180. package/lib/docs/installHeadlessShell.js +123 -0
  181. package/lib/docs/installHeadlessShell.test.mjs +70 -0
  182. package/lib/docs/installHeadlessShell.timeout.test.mjs +72 -0
  183. package/lib/docs/isHeadlessShellIncomplete.js +34 -0
  184. package/lib/docs/isHeadlessShellIncomplete.test.mjs +63 -0
  185. package/lib/docs/isPageReady.js +77 -0
  186. package/lib/docs/isPageReady.test.mjs +235 -0
  187. package/lib/docs/isRunErrorEntry.js +25 -0
  188. package/lib/docs/journeyMutantCookie.chromium.test.mjs +150 -0
  189. package/lib/docs/journeyRunRecording.js +35 -0
  190. package/lib/docs/journeyRunRecording.test.mjs +39 -0
  191. package/lib/docs/keepEventStreamAlive.js +68 -0
  192. package/lib/docs/keepEventStreamAlive.test.mjs +77 -0
  193. package/lib/docs/launchBrowser.js +83 -0
  194. package/lib/docs/launchBrowser.test.mjs +175 -0
  195. package/lib/docs/listModuleDocEntries.js +69 -0
  196. package/lib/docs/listPackageDocFiles.js +67 -0
  197. package/lib/docs/listPluginDocEntries.js +111 -0
  198. package/lib/docs/listPlugins.js +9 -58
  199. package/lib/docs/listTypes.js +14 -6
  200. package/lib/docs/loadFrameworkHazards.js +40 -0
  201. package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
  202. package/lib/docs/loadState.js +103 -35
  203. package/lib/docs/makeModuleManifestDoc.js +174 -0
  204. package/lib/docs/matchExercisedPages.js +35 -0
  205. package/lib/docs/matchJourneyRoute.js +69 -0
  206. package/lib/docs/matchJourneyRoute.test.mjs +178 -0
  207. package/lib/docs/mergeNetworkSnapshots.js +49 -0
  208. package/lib/docs/nextJourneySequence.js +30 -0
  209. package/lib/docs/noBrowserError.js +22 -0
  210. package/lib/docs/observe/TRANSIENT_SELECTOR.js +21 -0
  211. package/lib/docs/observe/claimExpectedErrors.js +67 -0
  212. package/lib/docs/observe/createJourneyAppErrors.js +236 -0
  213. package/lib/docs/observe/createJourneyAppErrors.test.mjs +385 -0
  214. package/lib/docs/observe/evaluateInvariants.js +183 -0
  215. package/lib/docs/observe/evaluateInvariants.test.mjs +299 -0
  216. package/lib/docs/observe/findingKey.js +29 -0
  217. package/lib/docs/observe/hasNoEffect.js +31 -0
  218. package/lib/docs/observe/hasNoEffect.test.mjs +37 -0
  219. package/lib/docs/observe/installStepObserver.js +72 -0
  220. package/lib/docs/observe/isAppApiUrl.js +33 -0
  221. package/lib/docs/observe/parseAppApiUrl.js +37 -0
  222. package/lib/docs/observe/readStepObserver.js +42 -0
  223. package/lib/docs/observe/relativeSource.js +29 -0
  224. package/lib/docs/observe/resolveConfigKeySource.js +33 -0
  225. package/lib/docs/observe/takeJourneyEvents.js +36 -0
  226. package/lib/docs/observe/toAppErrorFailure.js +61 -0
  227. package/lib/docs/observe/usesSearchStage.js +45 -0
  228. package/lib/docs/observe/waitForClientErrorReports.js +31 -0
  229. package/lib/docs/observe/watchJourneyContext.js +75 -0
  230. package/lib/docs/observe/withAppErrors.js +31 -0
  231. package/lib/docs/observe/withAppErrors.test.mjs +101 -0
  232. package/lib/docs/openJourney.js +94 -0
  233. package/lib/docs/openJourneyEmail.js +41 -0
  234. package/lib/docs/openJourneyEmail.test.mjs +187 -0
  235. package/lib/docs/pageBlocks.js +47 -0
  236. package/lib/docs/pageBuildStatus.test.mjs +103 -0
  237. package/lib/{client/setPageId.js → docs/parseModuleSource.js} +11 -12
  238. package/lib/docs/rails/authEngineActions.js +53 -0
  239. package/lib/docs/rails/authEngineActions.test.mjs +69 -0
  240. package/lib/docs/rails/authEngineMethods.js +52 -0
  241. package/lib/docs/rails/authEngineMethods.test.mjs +36 -0
  242. package/lib/docs/rails/checkAuthActionsRan.js +66 -0
  243. package/lib/docs/rails/checkAuthActionsRan.test.mjs +149 -0
  244. package/lib/docs/rails/checkClickRails.js +56 -0
  245. package/lib/docs/rails/createDataSetRails.js +82 -0
  246. package/lib/docs/rails/createDataSetRails.test.mjs +172 -0
  247. package/lib/docs/rails/findExternalBlocks.js +147 -0
  248. package/lib/docs/rails/findExternalBlocks.test.mjs +254 -0
  249. package/lib/docs/rails/installAuthActionRecorder.js +56 -0
  250. package/lib/docs/rails/readAuthActionRecorder.js +32 -0
  251. package/lib/docs/rails/readBuiltArtifact.js +30 -0
  252. package/lib/docs/readConnectionPlugins.js +26 -0
  253. package/lib/docs/readDevAuthMode.js +32 -0
  254. package/lib/docs/readDocEntry.js +40 -0
  255. package/lib/docs/readDocTitle.js +26 -0
  256. package/lib/docs/readJourneyEmailMatch.js +57 -0
  257. package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
  258. package/lib/docs/readJourneySession.js +56 -0
  259. package/lib/docs/readMailOutbox.js +37 -0
  260. package/lib/docs/readPagePath.js +27 -0
  261. package/lib/docs/readPluginPackages.js +99 -0
  262. package/lib/docs/readProxyBuildWait.js +38 -0
  263. package/lib/docs/readRoutineSteps.js +45 -0
  264. package/lib/docs/registerShownTextEngine.js +118 -0
  265. package/lib/docs/requestRestart.js +30 -0
  266. package/lib/docs/requestRestart.test.mjs +49 -0
  267. package/lib/docs/resolvePageInstance.js +46 -0
  268. package/lib/docs/resolvePageInstance.test.mjs +81 -0
  269. package/lib/docs/resolveSource.js +11 -7
  270. package/lib/docs/resolveSource.test.mjs +1 -1
  271. package/lib/docs/resolveToolCaller.js +62 -0
  272. package/lib/docs/reviewPage.js +51 -0
  273. package/lib/docs/reviewPageBuilds.js +56 -0
  274. package/lib/docs/reviewPageBuilds.test.mjs +207 -0
  275. package/lib/docs/runCheck.js +75 -0
  276. package/lib/docs/runCheck.test.mjs +91 -0
  277. package/lib/docs/runCheckChild.js +49 -0
  278. package/lib/docs/runEndpoint.js +185 -0
  279. package/lib/docs/runEndpoint.test.mjs +509 -0
  280. package/lib/docs/runErrorBuffers.js +99 -0
  281. package/lib/docs/runErrorBuffers.test.mjs +95 -0
  282. package/lib/docs/runJourney.js +347 -0
  283. package/lib/docs/runJourney.test.mjs +2383 -0
  284. package/lib/docs/runJourneyAsUsers.js +44 -0
  285. package/lib/docs/runJourneyDataSet.test.mjs +274 -0
  286. package/lib/docs/runJourneySteps.js +1320 -0
  287. package/lib/docs/runRequest.js +100 -59
  288. package/lib/docs/runRequest.test.mjs +190 -0
  289. package/lib/docs/saveAnnotatedScreenshot.js +1 -1
  290. package/lib/docs/scaffoldPage.js +3 -1
  291. package/lib/docs/screenshotPage.js +163 -11
  292. package/lib/docs/screenshotPage.test.mjs +263 -1
  293. package/lib/docs/screenshotPageClip.test.mjs +4 -0
  294. package/lib/docs/searchDocs.js +82 -30
  295. package/lib/docs/searchDocs.test.mjs +100 -0
  296. package/lib/docs/searchIcons.js +90 -0
  297. package/lib/docs/searchIcons.test.mjs +127 -0
  298. package/lib/docs/selectExercised.js +39 -0
  299. package/lib/docs/selectFinalState.js +66 -0
  300. package/lib/docs/selectFinalState.test.mjs +51 -0
  301. package/lib/docs/serverErrorStore.js +54 -0
  302. package/lib/docs/serverErrorStore.test.mjs +50 -0
  303. package/lib/docs/setupTestFixtures.mjs +133 -39
  304. package/lib/docs/shownTextSelector.js +28 -0
  305. package/lib/docs/snapshotState.js +13 -5
  306. package/lib/docs/splitSearchTerms.js +62 -0
  307. package/lib/docs/splitSearchTerms.test.mjs +50 -0
  308. package/lib/docs/stateCheckpoints.test.mjs +77 -0
  309. package/lib/docs/tabAvailable.js +6 -5
  310. package/lib/docs/tabChannel.js +127 -21
  311. package/lib/docs/tabChannel.test.mjs +187 -29
  312. package/lib/docs/unsettledPageNote.js +24 -0
  313. package/lib/docs/validateJourneyMail.js +36 -0
  314. package/lib/docs/validateJourneyTimeout.js +38 -0
  315. package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
  316. package/lib/docs/validateRecordingParam.js +42 -0
  317. package/lib/docs/validateStateSelection.js +33 -0
  318. package/lib/docs/validateStateSelection.test.mjs +33 -0
  319. package/lib/docs/validateViewport.js +51 -0
  320. package/lib/docs/validateViewport.test.mjs +54 -0
  321. package/lib/docs/waitForBuild.js +50 -0
  322. package/lib/docs/waitForBuild.test.mjs +75 -0
  323. package/lib/docs/waitForJourneyEmail.js +77 -0
  324. package/lib/docs/withBrowserSlot.js +45 -0
  325. package/lib/docs/withBrowserSlot.test.mjs +75 -0
  326. package/lib/docs/withDataSession.js +53 -0
  327. package/lib/server/applyDataSetRedirect.js +83 -0
  328. package/lib/server/applyDataSetRedirect.test.mjs +340 -0
  329. package/lib/server/applyDataSetRedirectTenantReach.test.mjs +204 -0
  330. package/lib/server/applyDataSetRedirectWalled.test.mjs +131 -0
  331. package/lib/server/auth/createSystemContext.js +73 -0
  332. package/lib/server/auth/{getAuthConfig.js → getAuth.js} +15 -9
  333. package/lib/server/auth/getHeadlessUser.js +9 -8
  334. package/lib/server/auth/getHeadlessUser.test.mjs +41 -0
  335. package/lib/server/auth/getMockUser.js +18 -14
  336. package/lib/server/auth/{session.js → getStrategies.js} +17 -16
  337. package/lib/server/auth/headlessUser.js +3 -2
  338. package/lib/server/auth/journeyActor.js +35 -0
  339. package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
  340. package/lib/server/checkPageRecord.js +44 -0
  341. package/lib/server/contextMapBudget.js +24 -0
  342. package/lib/server/contextMapBudget.test.mjs +98 -0
  343. package/lib/server/continueJitKeys.js +28 -0
  344. package/lib/server/continueJitKeys.test.mjs +43 -0
  345. package/lib/server/createDataSessionEndedError.js +33 -0
  346. package/lib/server/createDataSetRefusal.js +26 -0
  347. package/lib/server/createLowdefyContext.js +168 -41
  348. package/lib/server/createLowdefyContext.test.mjs +328 -8
  349. package/lib/server/getClientAddress.js +51 -0
  350. package/lib/server/getClientAddress.test.mjs +56 -0
  351. package/lib/server/getPageJitEnrichment.test.mjs +29 -11
  352. package/lib/server/hashConfigContent.js +26 -0
  353. package/lib/server/importAppCode.test.mjs +57 -0
  354. package/lib/server/isRoutineStep.js +27 -0
  355. package/lib/server/jitPageBuilder.js +309 -126
  356. package/lib/server/jitPageBuilder.skipPluginChecks.test.mjs +78 -0
  357. package/lib/server/jitPageBuilder.test.mjs +218 -0
  358. package/lib/server/journeyCookies/JOURNEY_COOKIES.js +37 -0
  359. package/lib/server/journeyCookies/forwardJourneyCookies.js +46 -0
  360. package/lib/server/journeyCookies/readJourneyCookie.js +52 -0
  361. package/lib/server/journeyCookies/writeJourneyCookie.js +46 -0
  362. package/lib/server/journeyCookies.js +22 -0
  363. package/lib/server/journeyCookies.test.mjs +145 -0
  364. package/lib/server/loadDynamicJsMap.js +47 -0
  365. package/lib/server/log/createHandleError.js +38 -2
  366. package/lib/server/log/createHandleError.test.mjs +125 -2
  367. package/lib/server/log/createLogger.js +3 -0
  368. package/lib/server/log/createLogger.test.mjs +15 -0
  369. package/lib/server/log/logRequest.js +2 -3
  370. package/lib/server/mutantDataSetWrappers.test.mjs +112 -0
  371. package/lib/server/mutants/applyMutant.js +39 -0
  372. package/lib/server/mutants/artifactPatterns.js +25 -0
  373. package/lib/server/mutants/buildAnchor.js +45 -0
  374. package/lib/server/mutants/collectSharedIds.js +41 -0
  375. package/lib/server/mutants/configPathSegments.js +38 -0
  376. package/lib/server/mutants/createMutantReadConfigFile.js +42 -0
  377. package/lib/server/mutants/describeEvent.js +22 -0
  378. package/lib/server/mutants/enumerateArtifact.js +56 -0
  379. package/lib/server/mutants/findNodeByKey.js +45 -0
  380. package/lib/server/mutants/findNodeByPointer.js +49 -0
  381. package/lib/server/mutants/getArtifactKind.js +32 -0
  382. package/lib/server/mutants/groupMutantCopies.js +56 -0
  383. package/lib/server/mutants/hashArtifact.js +25 -0
  384. package/lib/server/mutants/hashMutantTarget.js +34 -0
  385. package/lib/server/mutants/listMutants.js +122 -0
  386. package/lib/server/mutants/listMutants.test.mjs +502 -0
  387. package/lib/server/mutants/mutantId.js +30 -0
  388. package/lib/server/mutants/mutantRuns.js +57 -0
  389. package/lib/server/mutants/mutantRuns.test.mjs +136 -0
  390. package/lib/server/mutants/operators/dropAction.js +47 -0
  391. package/lib/server/mutants/operators/dropBlock.js +44 -0
  392. package/lib/server/mutants/operators/dropPayload.js +52 -0
  393. package/lib/server/mutants/operators/dropStep.js +44 -0
  394. package/lib/server/mutants/operators/flipVisible.js +53 -0
  395. package/lib/server/mutants/operators/index.js +37 -0
  396. package/lib/server/mutants/operators/retargetLink.js +72 -0
  397. package/lib/server/mutants/operators/skipValidate.js +77 -0
  398. package/lib/server/mutants/operators/swapIf.js +112 -0
  399. package/lib/server/mutants/operators.test.mjs +396 -0
  400. package/lib/server/mutants/positionFreePath.js +38 -0
  401. package/lib/server/mutants/removeFromParent.js +28 -0
  402. package/lib/server/mutants/test/fixtures.mjs +173 -0
  403. package/lib/server/mutants/test/keyArtifact.mjs +63 -0
  404. package/lib/server/mutants/validateMutantParam.js +69 -0
  405. package/lib/server/mutants/validateMutantsBody.js +88 -0
  406. package/lib/server/mutants/walkArtifact.js +179 -0
  407. package/lib/server/pageBuildRecords.js +131 -0
  408. package/lib/server/pageBuildRecords.test.mjs +258 -0
  409. package/lib/server/pageCache.mjs +22 -7
  410. package/lib/server/pageCache.test.mjs +22 -7
  411. package/lib/server/pageRebuilds.test.mjs +543 -0
  412. package/lib/server/pruneJitMaps.js +36 -0
  413. package/lib/server/pruneJitMaps.test.mjs +55 -0
  414. package/lib/server/readInstanceRecord.js +33 -0
  415. package/lib/server/readInstanceRecord.test.mjs +99 -0
  416. package/lib/server/readMergedMaps.js +86 -0
  417. package/lib/server/readMergedMaps.test.mjs +79 -0
  418. package/lib/server/recording/appendRecords.js +33 -0
  419. package/lib/server/recording/errorRecordingStamp.js +34 -0
  420. package/lib/server/recording/errorRecordingStamp.test.mjs +31 -0
  421. package/lib/server/recording/isRecordingEnabled.js +31 -0
  422. package/lib/server/recording/readRecordingCookie.js +71 -0
  423. package/lib/server/recording/readRecordingCookie.test.mjs +73 -0
  424. package/lib/server/recording/recordingCookiePayload.js +33 -0
  425. package/lib/server/recording/servedBuilds.js +46 -0
  426. package/lib/server/recording/servedBuilds.test.mjs +42 -0
  427. package/lib/server/sinkExternalConnections.js +95 -0
  428. package/lib/server/sinkExternalConnections.test.mjs +135 -0
  429. package/lib/server/skipStaleMapWrites.js +62 -0
  430. package/lib/server/skipStaleMapWrites.test.mjs +113 -0
  431. package/lib/server/startIdleGc.js +67 -0
  432. package/lib/server/startIdleGc.test.mjs +138 -0
  433. package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
  434. package/lib/server/trackSessionWork.js +30 -0
  435. package/lib/vite/createDevServerExclude.js +40 -0
  436. package/lib/vite/createDevServerExclude.test.mjs +79 -0
  437. package/manager/getContext.mjs +24 -2
  438. package/manager/processes/initialBuild.mjs +14 -0
  439. package/manager/processes/initialBuild.test.mjs +101 -0
  440. package/manager/processes/lowdefyBuild.mjs +30 -7
  441. package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +62 -0
  442. package/manager/processes/lowdefyBuild.test.mjs +136 -1
  443. package/manager/processes/optimizeDependencies.mjs +59 -0
  444. package/manager/processes/restartServer.mjs +34 -6
  445. package/manager/processes/restartServer.test.mjs +162 -0
  446. package/manager/processes/shutdownServer.mjs +11 -0
  447. package/manager/processes/shutdownServer.test.mjs +45 -0
  448. package/manager/processes/startFirstServer.mjs +46 -0
  449. package/manager/processes/startFirstServer.test.mjs +326 -0
  450. package/manager/processes/startMailSink.mjs +135 -0
  451. package/manager/processes/startMailSink.test.mjs +146 -0
  452. package/manager/processes/startProxy.mjs +129 -12
  453. package/manager/processes/startProxy.test.mjs +366 -0
  454. package/manager/processes/startServer.mjs +47 -6
  455. package/manager/processes/startServer.test.mjs +155 -0
  456. package/manager/processes/startWatchers.mjs +6 -4
  457. package/manager/processes/syncServer.mjs +69 -0
  458. package/manager/processes/syncServer.test.mjs +141 -0
  459. package/manager/processes/waitForDevServer.mjs +48 -0
  460. package/manager/processes/waitForDevServer.test.mjs +112 -0
  461. package/manager/run.mjs +128 -43
  462. package/manager/utils/BatchChanges.mjs +21 -5
  463. package/manager/utils/BatchChanges.test.mjs +77 -0
  464. package/manager/utils/acquireDevInstance.mjs +99 -0
  465. package/manager/utils/acquireDevInstance.test.mjs +160 -0
  466. package/manager/utils/createBuildActivity.mjs +67 -0
  467. package/manager/utils/createBuildActivity.test.mjs +58 -0
  468. package/manager/utils/createChangeSignal.mjs +39 -0
  469. package/manager/utils/createChangeSignal.test.mjs +62 -0
  470. package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
  471. package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
  472. package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
  473. package/manager/utils/createDotPathIgnore.mjs +44 -0
  474. package/manager/utils/createDotPathIgnore.test.mjs +65 -0
  475. package/manager/utils/createIdleGc.mjs +51 -0
  476. package/manager/utils/createIdleGc.test.mjs +77 -0
  477. package/manager/utils/createRequestActivity.mjs +100 -0
  478. package/manager/utils/createRequestActivity.test.mjs +121 -0
  479. package/manager/utils/createServerArtifactTracker.mjs +83 -0
  480. package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
  481. package/manager/utils/createServerEnv.mjs +39 -0
  482. package/manager/utils/filesHaveSameBytes.mjs +41 -0
  483. package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
  484. package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
  485. package/manager/utils/importFresh.mjs +46 -0
  486. package/manager/utils/importFresh.test.mjs +72 -0
  487. package/manager/utils/importFreshWorker.mjs +20 -0
  488. package/manager/utils/killTaggedBrowser.mjs +35 -0
  489. package/manager/utils/pagePathChanged.mjs +56 -0
  490. package/manager/utils/pruneRecordings.mjs +85 -0
  491. package/manager/utils/pruneRecordings.test.mjs +117 -0
  492. package/manager/utils/publishBuildDirectory.mjs +121 -0
  493. package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
  494. package/manager/utils/publishBuildDirectory.test.mjs +262 -0
  495. package/manager/utils/readBasePath.mjs +27 -0
  496. package/manager/utils/readBasePath.test.mjs +43 -0
  497. package/manager/utils/readBuildStatusWait.mjs +67 -0
  498. package/manager/utils/readManagerVersion.mjs +30 -0
  499. package/manager/utils/readManagerVersion.test.mjs +35 -0
  500. package/manager/utils/readPluginDefinitions.mjs +36 -0
  501. package/manager/utils/resolveDevAuthUrl.mjs +58 -0
  502. package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
  503. package/manager/utils/resolvePorts.mjs +49 -0
  504. package/manager/utils/setupWatcher.mjs +19 -11
  505. package/manager/utils/startRecordingPruner.mjs +45 -0
  506. package/manager/utils/startRecordingPruner.test.mjs +66 -0
  507. package/manager/utils/waitForChildExit.mjs +53 -0
  508. package/manager/utils/waitForChildExit.test.mjs +70 -0
  509. package/manager/utils/waitForServer.mjs +44 -0
  510. package/manager/utils/writeBuildStatus.mjs +1 -1
  511. package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
  512. package/manager/watchers/envWatcher.mjs +9 -2
  513. package/manager/watchers/envWatcher.test.mjs +87 -0
  514. package/manager/watchers/listServerSidePackages.mjs +47 -0
  515. package/manager/watchers/listServerSidePackages.test.mjs +38 -0
  516. package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
  517. package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
  518. package/manager/watchers/pluginSourceWatcher.mjs +79 -0
  519. package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
  520. package/manager/watchers/restartRequestWatcher.mjs +71 -0
  521. package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
  522. package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
  523. package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
  524. package/manager/watchers/serverPackageWatcher.mjs +38 -0
  525. package/package.json +60 -47
  526. package/package.original.json +60 -47
  527. package/src/app.js +131 -20
  528. package/src/html/renderDevPage.js +63 -11
  529. package/src/html/renderDevPage.test.mjs +138 -0
  530. package/src/lib/getRequestPath.js +29 -0
  531. package/src/middleware/createSameOriginGuard.js +37 -0
  532. package/src/middleware/createSameOriginGuard.test.mjs +102 -0
  533. package/src/middleware/errorHandler.js +51 -1
  534. package/src/middleware/errorHandler.test.mjs +173 -3
  535. package/src/middleware/isRebindingSafeHost.js +44 -0
  536. package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
  537. package/src/middleware/localDevToolsOnly.js +38 -0
  538. package/src/middleware/localDevToolsOnly.test.mjs +61 -0
  539. package/src/middleware/noStore.js +27 -0
  540. package/src/middleware/noStore.test.mjs +48 -0
  541. package/src/middleware/requestTimeout.js +38 -0
  542. package/src/middleware/staleFlag.js +76 -0
  543. package/src/middleware/staleFlag.test.mjs +127 -0
  544. package/src/middleware/stampCaller.js +38 -0
  545. package/src/middleware/stampCaller.test.mjs +64 -0
  546. package/src/routes/agent.js +15 -3
  547. package/src/routes/auth.js +29 -20
  548. package/src/routes/auth.test.mjs +30 -56
  549. package/src/routes/clientError.js +13 -13
  550. package/src/routes/cron.js +3 -1
  551. package/src/routes/cronForward.js +3 -1
  552. package/src/routes/detached.js +22 -9
  553. package/src/routes/devInspect.js +17 -8
  554. package/src/routes/devInspect.test.mjs +44 -7
  555. package/src/routes/devRecording.js +113 -0
  556. package/src/routes/devRecording.test.mjs +253 -0
  557. package/src/routes/docs/buildStatus.js +10 -2
  558. package/src/routes/docs/buildStatus.test.mjs +46 -0
  559. package/src/routes/docs/content.js +5 -3
  560. package/src/routes/docs/evalOperator.js +15 -1
  561. package/src/routes/docs/events.js +50 -0
  562. package/src/routes/docs/events.test.mjs +117 -0
  563. package/src/routes/docs/find.js +9 -1
  564. package/src/routes/docs/icons.js +30 -0
  565. package/src/routes/docs/inspectState.js +16 -1
  566. package/src/routes/docs/journey.js +194 -0
  567. package/src/routes/docs/journey.test.mjs +401 -0
  568. package/src/routes/docs/journeyRefusals.test.mjs +71 -0
  569. package/src/routes/docs/loadState.js +4 -2
  570. package/src/routes/docs/mcp.js +34 -3
  571. package/src/routes/docs/mcp.test.mjs +130 -0
  572. package/src/routes/docs/mutants.js +78 -0
  573. package/src/routes/docs/mutants.test.mjs +180 -0
  574. package/src/routes/docs/parsePathParamsParam.js +41 -0
  575. package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
  576. package/src/routes/docs/parseUserParam.js +27 -21
  577. package/src/routes/docs/parseUserParam.test.mjs +17 -5
  578. package/src/routes/docs/restart.js +35 -0
  579. package/src/routes/docs/restart.test.mjs +64 -0
  580. package/src/routes/docs/runEndpoint.js +54 -0
  581. package/src/routes/docs/runEndpoint.test.mjs +165 -0
  582. package/src/routes/docs/runRequest.js +25 -8
  583. package/src/routes/docs/runRequest.test.mjs +92 -0
  584. package/src/routes/docs/screenshot.js +50 -2
  585. package/src/routes/docs/screenshot.test.mjs +117 -0
  586. package/src/routes/docs/search.js +15 -2
  587. package/src/routes/docs/snapshotState.js +17 -3
  588. package/src/routes/endpoints.js +27 -26
  589. package/src/routes/endpointsTextAnswer.test.mjs +77 -0
  590. package/src/routes/feedback.js +10 -13
  591. package/src/routes/jitPage.js +99 -35
  592. package/src/routes/jitPage.test.mjs +248 -0
  593. package/src/routes/mcp.js +82 -5
  594. package/src/routes/mcp.test.mjs +215 -0
  595. package/src/routes/mcpMemberToken.test.mjs +125 -0
  596. package/src/routes/mcpProtectedResourceMetadata.js +34 -0
  597. package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
  598. package/src/routes/mountDevPageRoutes.js +25 -0
  599. package/src/routes/mountDevPageRoutes.test.mjs +30 -0
  600. package/src/routes/mountOauthDiscovery.js +44 -0
  601. package/src/routes/mountOauthDiscovery.test.mjs +79 -0
  602. package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
  603. package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
  604. package/src/routes/reload.js +16 -0
  605. package/src/routes/request.js +6 -4
  606. package/src/routes/request.test.mjs +9 -5
  607. package/src/routes/requestBody.test.mjs +159 -0
  608. package/src/routes/usage.js +6 -2
  609. package/src/routes/user.js +27 -0
  610. package/src/routes/websocket.js +29 -8
  611. package/src/routes/websocket.test.mjs +65 -0
  612. package/src/routes/wellKnownFallback.js +30 -0
  613. package/src/routes/wellKnownFallback.test.mjs +32 -0
  614. package/src/websocket/devWebSocket.js +41 -26
  615. package/src/websocket/devWebSocket.test.mjs +157 -0
  616. package/vite.config.js +53 -23
  617. package/lib/server/auth/getDevSession.js +0 -70
  618. package/lib/server/auth/getDevSession.test.mjs +0 -138
  619. package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
  620. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
  621. package/manager/watchers/moduleBuildWatcher.mjs +0 -79
  622. package/manager/watchers/serverArtifactWatcher.mjs +0 -120
  623. package/src/routes/apiPage.js +0 -37
@@ -0,0 +1,183 @@
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 { expectedErrorNames } from '@lowdefy/errors';
18
+ import { type } from '@lowdefy/helpers';
19
+ import { getStepKey } from '@lowdefy/node-utils';
20
+
21
+ import findingKey from './findingKey.js';
22
+ import hasNoEffect from './hasNoEffect.js';
23
+ import parseAppApiUrl from './parseAppApiUrl.js';
24
+ import relativeSource from './relativeSource.js';
25
+
26
+ const SEVERITY = {
27
+ 'action-error': 'error',
28
+ 'client-error': 'error',
29
+ 'server-error': 'error',
30
+ 'request-failed': 'error',
31
+ 'dead-click': 'warning',
32
+ environment: 'info',
33
+ };
34
+
35
+ function describeTarget(target) {
36
+ const parts = [target.blockId ?? 'control'];
37
+ if (!type.isNone(target.text)) parts.push(`"${target.text}"`);
38
+ if (!type.isNone(target.row)) parts.push(`row ${target.row}`);
39
+ return parts.join(' ');
40
+ }
41
+
42
+ function isExplained({ response, serverEntries, basePath }) {
43
+ const called = parseAppApiUrl({ url: response.url, basePath });
44
+ return serverEntries.some(
45
+ (entry) =>
46
+ (!type.isNone(called?.requestId) &&
47
+ entry.requestId === called.requestId &&
48
+ entry.pageId === called.pageId) ||
49
+ (!type.isNone(called?.endpointId) && entry.endpointId === called.endpointId)
50
+ );
51
+ }
52
+
53
+ function isDeadClick({ step, result, window }) {
54
+ return !type.isUndefined(step.click) && result.status === 'ok' && hasNoEffect({ window });
55
+ }
56
+
57
+ // The fixed checks that decide whether a journey step broke something, over what
58
+ // the step's window produced for this run only:
59
+ // action-error an event failed with an error that is not an expected
60
+ // outcome: a failed Validate (a UserError) or an auth gate's
61
+ // 401 or 403 refusal is the app doing its job
62
+ // client-error an uncaught page error, or a client error entry this run
63
+ // caused
64
+ // server-error a server error entry this run caused
65
+ // request-failed a 5xx from the app's request or endpoint routes that no
66
+ // server error entry in the window explains
67
+ // dead-click a click that succeeded and changed nothing: no event, no
68
+ // lasting DOM change, no app request, no URL change
69
+ // environment a server error from a request using a $search or
70
+ // $vectorSearch stage, which the data set's memory store
71
+ // cannot run: not the app's fault
72
+ // window: { emits, mutationCount (null when the document was replaced),
73
+ // urlBefore, urlAfter, pageErrors, requests, responses, errors (run-claimed
74
+ // error entries, each with store 'client' or 'server') }. usesSearchStage
75
+ // (entry) and resolveSource(configKey) read the dev server's build. Sources
76
+ // are made relative to configDirectory. Returns findings { kind, severity,
77
+ // message, pageId, source, configKey, key }, one per key.
78
+ // The browser's TypeError for a fetch that did not complete, in each engine.
79
+ const FETCH_FAILURE =
80
+ /^(TypeError: )?(Failed to fetch|NetworkError when attempting to fetch|Load failed)/;
81
+
82
+ // A goto replaces the page, and the browser aborts the requests the page left
83
+ // behind had in flight; the page reports each as a failed fetch. That is the
84
+ // runner's own navigation, not an app error, so a goto step's fetch failures
85
+ // are dropped when the window also saw the browser abort a request. A failed
86
+ // fetch with no aborted request, or in any other step, is still an error.
87
+ function isCausedByGoto({ step, window, message }) {
88
+ return (
89
+ getStepKey(step ?? {}) === 'goto' &&
90
+ (window.abortedRequests ?? []).length > 0 &&
91
+ FETCH_FAILURE.test(message ?? '')
92
+ );
93
+ }
94
+
95
+ async function evaluateInvariants({
96
+ step,
97
+ result,
98
+ window,
99
+ pageId,
100
+ basePath = '',
101
+ configDirectory,
102
+ usesSearchStage,
103
+ resolveSource,
104
+ }) {
105
+ let findings = [];
106
+ function add({ kind, message, source = null, configKey = null }) {
107
+ findings.push({
108
+ kind,
109
+ severity: SEVERITY[kind],
110
+ message,
111
+ pageId,
112
+ source: relativeSource({ source, configDirectory }),
113
+ configKey,
114
+ });
115
+ }
116
+
117
+ for (const emit of window.emits) {
118
+ if (emit.success !== false || expectedErrorNames.has(emit.failure?.errorName)) continue;
119
+ const { actionId, actionType, configKey, errorName } = emit.failure ?? {};
120
+ add({
121
+ kind: 'action-error',
122
+ message: `${actionType ?? 'An action'} "${actionId ?? 'unknown'}" failed in ${emit.blockId}.${
123
+ emit.eventName
124
+ } with ${errorName ?? 'an error'}.`,
125
+ configKey: configKey ?? null,
126
+ source: type.isNone(configKey) ? null : await resolveSource(configKey),
127
+ });
128
+ }
129
+
130
+ const clientEntries = window.errors
131
+ .filter((entry) => entry.store === 'client')
132
+ .filter((entry) => !isCausedByGoto({ step, window, message: entry.message ?? entry.name }));
133
+ const serverEntries = window.errors.filter((entry) => entry.store === 'server');
134
+ clientEntries.forEach((entry) => {
135
+ add({ kind: 'client-error', message: entry.message ?? entry.name, source: entry.source });
136
+ });
137
+ window.pageErrors
138
+ .filter((pageError) => !isCausedByGoto({ step, window, message: pageError.message }))
139
+ .filter((pageError) => !clientEntries.some((entry) => entry.message === pageError.message))
140
+ .forEach((pageError) => {
141
+ add({ kind: 'client-error', message: `${pageError.name}: ${pageError.message}` });
142
+ });
143
+ serverEntries.forEach((entry) => {
144
+ add({
145
+ kind: usesSearchStage(entry) ? 'environment' : 'server-error',
146
+ message: entry.message ?? entry.name,
147
+ source: entry.source,
148
+ });
149
+ });
150
+ window.responses
151
+ .filter((response) => response.status >= 500)
152
+ .filter((response) => !isExplained({ response, serverEntries, basePath }))
153
+ .forEach((response) => {
154
+ add({
155
+ kind: 'request-failed',
156
+ message: `${response.method} ${new URL(response.url).pathname} answered ${
157
+ response.status
158
+ }.`,
159
+ });
160
+ });
161
+ if (isDeadClick({ step, result, window })) {
162
+ add({
163
+ kind: 'dead-click',
164
+ message: `Clicking ${describeTarget(
165
+ step.click
166
+ )} did nothing: no event ran, the page did not change and no request was sent.`,
167
+ });
168
+ }
169
+
170
+ if (findings.some((finding) => finding.kind === 'environment')) {
171
+ findings = findings.filter((finding) => finding.kind === 'environment');
172
+ }
173
+ const seen = new Set();
174
+ return findings
175
+ .map((finding) => ({ ...finding, key: findingKey(finding) }))
176
+ .filter((finding) => {
177
+ if (seen.has(finding.key)) return false;
178
+ seen.add(finding.key);
179
+ return true;
180
+ });
181
+ }
182
+
183
+ export default evaluateInvariants;
@@ -0,0 +1,299 @@
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 evaluateInvariants from './evaluateInvariants.js';
18
+
19
+ const origin = 'http://localhost:3111';
20
+
21
+ function quietWindow(overrides = {}) {
22
+ return {
23
+ urlBefore: `${origin}/tickets`,
24
+ urlAfter: `${origin}/tickets`,
25
+ emits: [],
26
+ mutationCount: 0,
27
+ pageErrors: [],
28
+ requests: [],
29
+ responses: [],
30
+ errors: [],
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ function evaluate({
36
+ step = { click: { blockId: 'assign_button' } },
37
+ result,
38
+ window,
39
+ search = false,
40
+ }) {
41
+ return evaluateInvariants({
42
+ step,
43
+ result: result ?? { status: 'ok' },
44
+ window,
45
+ pageId: 'tickets',
46
+ usesSearchStage: () => search,
47
+ resolveSource: async (configKey) => (configKey === 'k1' ? 'pages/tickets.yaml:88' : null),
48
+ });
49
+ }
50
+
51
+ function failedEmit(errorName, extra = {}) {
52
+ return {
53
+ blockId: 'assign_button',
54
+ eventName: 'onClick',
55
+ success: false,
56
+ failure: { actionId: 'assign', actionType: 'CallAPI', configKey: 'k1', errorName, ...extra },
57
+ };
58
+ }
59
+
60
+ test('a failed event with an error that is not a UserError is an action-error located through its config key', async () => {
61
+ const findings = await evaluate({
62
+ window: quietWindow({ emits: [failedEmit('RequestError')], mutationCount: 3 }),
63
+ });
64
+ expect(findings).toEqual([
65
+ {
66
+ kind: 'action-error',
67
+ severity: 'error',
68
+ message: 'CallAPI "assign" failed in assign_button.onClick with RequestError.',
69
+ pageId: 'tickets',
70
+ source: 'pages/tickets.yaml:88',
71
+ configKey: 'k1',
72
+ key: 'action-error|tickets|pages/tickets.yaml:88',
73
+ },
74
+ ]);
75
+ });
76
+
77
+ test('a failed Validate (a UserError) is not a finding, and its click is not dead', async () => {
78
+ const findings = await evaluate({
79
+ window: quietWindow({ emits: [failedEmit('UserError', { actionType: 'Validate' })] }),
80
+ });
81
+ expect(findings).toEqual([]);
82
+ });
83
+
84
+ test.each(['AuthenticationError', 'AuthorizationError', 'TwoFactorEnrolmentRequiredError'])(
85
+ 'a CallAPI an auth gate refused (%s) is not a finding, and its click is not dead',
86
+ async (errorName) => {
87
+ const findings = await evaluate({
88
+ window: quietWindow({
89
+ emits: [failedEmit(errorName)],
90
+ requests: [{ method: 'POST', url: `${origin}/api/endpoints/assign` }],
91
+ responses: [
92
+ {
93
+ method: 'POST',
94
+ url: `${origin}/api/endpoints/assign`,
95
+ status: errorName === 'AuthenticationError' ? 401 : 403,
96
+ },
97
+ ],
98
+ }),
99
+ });
100
+ expect(findings).toEqual([]);
101
+ }
102
+ );
103
+
104
+ test('client error entries and uncaught page errors are client-errors; a page error the entry already reports is not repeated', async () => {
105
+ const findings = await evaluate({
106
+ window: quietWindow({
107
+ mutationCount: 1,
108
+ errors: [
109
+ {
110
+ store: 'client',
111
+ name: 'OperatorError',
112
+ message: 'Ticket boom',
113
+ source: 'pages/tickets.yaml:12',
114
+ },
115
+ ],
116
+ pageErrors: [
117
+ { name: 'Error', message: 'Ticket boom' },
118
+ { name: 'TypeError', message: 'x is undefined' },
119
+ ],
120
+ }),
121
+ });
122
+ expect(findings.map(({ kind, message, source }) => [kind, message, source])).toEqual([
123
+ ['client-error', 'Ticket boom', 'pages/tickets.yaml:12'],
124
+ ['client-error', 'TypeError: x is undefined', null],
125
+ ]);
126
+ });
127
+
128
+ test('a window with an environment finding reports only that, not the failed action or 5xx it caused', async () => {
129
+ const response = { url: `${origin}/api/request/tickets/search`, method: 'POST', status: 500 };
130
+ const findings = await evaluate({
131
+ search: true,
132
+ window: quietWindow({
133
+ emits: [failedEmit('ServiceError', { actionType: 'Request' })],
134
+ requests: [response],
135
+ responses: [response],
136
+ errors: [{ store: 'server', message: 'Atlas only', pageId: 'other', requestId: 'x' }],
137
+ }),
138
+ });
139
+ expect(findings.map((finding) => finding.kind)).toEqual(['environment']);
140
+ });
141
+
142
+ test('an absolute source is made relative to the config directory', async () => {
143
+ const findings = await evaluateInvariants({
144
+ step: { click: { blockId: 'a' } },
145
+ result: { status: 'ok' },
146
+ window: quietWindow({
147
+ mutationCount: 1,
148
+ errors: [{ store: 'server', message: 'Boom', source: '/apps/crm/pages/tickets.yaml:30' }],
149
+ }),
150
+ pageId: 'tickets',
151
+ configDirectory: '/apps/crm',
152
+ usesSearchStage: () => false,
153
+ resolveSource: async () => null,
154
+ });
155
+ expect(findings[0].source).toBe('pages/tickets.yaml:30');
156
+ expect(findings[0].key).toBe('server-error|tickets|pages/tickets.yaml:30');
157
+ });
158
+
159
+ test('a server error entry is a server-error, or environment when its request uses a search stage', async () => {
160
+ const window = quietWindow({
161
+ mutationCount: 1,
162
+ errors: [{ store: 'server', message: 'Unrecognized stage', source: 'pages/tickets.yaml:30' }],
163
+ });
164
+ expect((await evaluate({ window })).map((finding) => [finding.kind, finding.severity])).toEqual([
165
+ ['server-error', 'error'],
166
+ ]);
167
+ expect(
168
+ (await evaluate({ window, search: true })).map((finding) => [finding.kind, finding.severity])
169
+ ).toEqual([['environment', 'info']]);
170
+ });
171
+
172
+ test('a 5xx from an app route is request-failed unless a server error entry for that request explains it', async () => {
173
+ const response = {
174
+ url: `${origin}/api/endpoints/assign_ticket`,
175
+ method: 'POST',
176
+ status: 500,
177
+ };
178
+ const unexplained = await evaluate({
179
+ window: quietWindow({ requests: [response], responses: [response] }),
180
+ });
181
+ expect(unexplained).toEqual([
182
+ expect.objectContaining({
183
+ kind: 'request-failed',
184
+ message: 'POST /api/endpoints/assign_ticket answered 500.',
185
+ }),
186
+ ]);
187
+ const explained = await evaluate({
188
+ window: quietWindow({
189
+ requests: [response],
190
+ responses: [response],
191
+ errors: [{ store: 'server', message: 'Boom', endpointId: 'assign_ticket', source: null }],
192
+ }),
193
+ });
194
+ expect(explained.map((finding) => finding.kind)).toEqual(['server-error']);
195
+ const request = {
196
+ url: `${origin}/api/request/tickets/close_ticket`,
197
+ method: 'POST',
198
+ status: 502,
199
+ };
200
+ const explainedRequest = await evaluate({
201
+ window: quietWindow({
202
+ requests: [request],
203
+ responses: [request],
204
+ errors: [{ store: 'server', message: 'Down', pageId: 'tickets', requestId: 'close_ticket' }],
205
+ }),
206
+ });
207
+ expect(explainedRequest.map((finding) => finding.kind)).toEqual(['server-error']);
208
+ });
209
+
210
+ test('a successful click with no event, no lasting mutation, no app request and no URL change is a dead-click warning', async () => {
211
+ const findings = await evaluate({ window: quietWindow() });
212
+ expect(findings).toEqual([
213
+ expect.objectContaining({
214
+ kind: 'dead-click',
215
+ severity: 'warning',
216
+ message:
217
+ 'Clicking assign_button did nothing: no event ran, the page did not change and no request was sent.',
218
+ }),
219
+ ]);
220
+ });
221
+
222
+ test('a click is not dead when an event ran, the DOM changed, a request went out, the URL changed, the document was replaced or the step failed', async () => {
223
+ const lively = [
224
+ quietWindow({ emits: [{ blockId: 'a', eventName: 'onClick', success: true }] }),
225
+ quietWindow({ mutationCount: 1 }),
226
+ quietWindow({ requests: [{ url: `${origin}/api/request/tickets/x` }] }),
227
+ quietWindow({ urlAfter: `${origin}/ticket` }),
228
+ quietWindow({ mutationCount: null }),
229
+ ];
230
+ for (const window of lively) {
231
+ expect(await evaluate({ window })).toEqual([]);
232
+ }
233
+ expect(await evaluate({ window: quietWindow(), result: { status: 'failed' } })).toEqual([]);
234
+ expect(
235
+ await evaluate({ window: quietWindow(), step: { fill: { blockId: 'title', value: 'x' } } })
236
+ ).toEqual([]);
237
+ });
238
+
239
+ test('findings with the same key in one window are reported once', async () => {
240
+ const findings = await evaluate({
241
+ window: quietWindow({
242
+ mutationCount: 1,
243
+ errors: [
244
+ { store: 'server', message: 'Boom', source: 'pages/tickets.yaml:30' },
245
+ { store: 'server', message: 'Boom again', source: 'pages/tickets.yaml:30' },
246
+ ],
247
+ }),
248
+ });
249
+ expect(findings).toHaveLength(1);
250
+ });
251
+
252
+ describe('a fetch failure the runner caused by navigating away', () => {
253
+ const failedFetch = {
254
+ store: 'client',
255
+ name: 'TypeError',
256
+ message: 'Failed to fetch at onboarding-step-2.',
257
+ source: null,
258
+ };
259
+ const aborted = [{ time: 1, url: `${origin}/api/request/step/get_header` }];
260
+
261
+ test('a goto step that aborted requests in flight does not fail on the page reporting them', async () => {
262
+ const findings = await evaluate({
263
+ step: { goto: 'onboarding-step-connect' },
264
+ window: quietWindow({
265
+ errors: [failedFetch],
266
+ pageErrors: [{ name: 'TypeError', message: 'Failed to fetch' }],
267
+ abortedRequests: aborted,
268
+ }),
269
+ });
270
+ expect(findings).toEqual([]);
271
+ });
272
+
273
+ test('a failed fetch outside a goto is still a client-error, aborted requests or not', async () => {
274
+ const findings = await evaluate({
275
+ step: { click: { blockId: 'next_button' } },
276
+ window: quietWindow({ mutationCount: 1, errors: [failedFetch], abortedRequests: aborted }),
277
+ });
278
+ expect(findings.map(({ kind }) => kind)).toEqual(['client-error']);
279
+ });
280
+
281
+ test('a failed fetch in a goto that aborted nothing is still a client-error', async () => {
282
+ const findings = await evaluate({
283
+ step: { goto: 'onboarding-step-connect' },
284
+ window: quietWindow({ errors: [failedFetch] }),
285
+ });
286
+ expect(findings.map(({ kind }) => kind)).toEqual(['client-error']);
287
+ });
288
+
289
+ test('another error in a goto that aborted requests still fails the step', async () => {
290
+ const findings = await evaluate({
291
+ step: { goto: 'onboarding-step-connect' },
292
+ window: quietWindow({
293
+ errors: [failedFetch, { store: 'client', name: 'OperatorError', message: 'Ticket boom' }],
294
+ abortedRequests: aborted,
295
+ }),
296
+ });
297
+ expect(findings.map(({ message }) => message)).toEqual(['Ticket boom']);
298
+ });
299
+ });
@@ -0,0 +1,29 @@
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 crypto from 'node:crypto';
18
+
19
+ // One finding per (kind, page, source location), or per message when there
20
+ // is no location: a journey's app errors are reported, and compared across
21
+ // runs, by it.
22
+ function findingKey({ kind, pageId, source, message }) {
23
+ const where =
24
+ source ??
25
+ `message:${crypto.createHash('sha1').update(String(message)).digest('hex').slice(0, 8)}`;
26
+ return [kind, pageId ?? '', where].join('|');
27
+ }
28
+
29
+ export default findingKey;
@@ -0,0 +1,31 @@
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
+ // Whether a step's window shows the step did nothing: no event ran, no lasting
18
+ // DOM change (a mutationCount of null means the document was replaced), no
19
+ // request to the app's request or endpoint routes (the window holds only
20
+ // those) and the URL is unchanged. The dead-click invariant and the journey
21
+ // runner's expect.effect both read it, so the two cannot drift.
22
+ function hasNoEffect({ window }) {
23
+ return (
24
+ window.emits.length === 0 &&
25
+ window.mutationCount === 0 &&
26
+ window.requests.length === 0 &&
27
+ window.urlBefore === window.urlAfter
28
+ );
29
+ }
30
+
31
+ export default hasNoEffect;
@@ -0,0 +1,37 @@
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 hasNoEffect from './hasNoEffect.js';
18
+
19
+ const url = 'http://localhost:3227/orders';
20
+
21
+ function windowWith(overrides) {
22
+ return { emits: [], mutationCount: 0, requests: [], urlBefore: url, urlAfter: url, ...overrides };
23
+ }
24
+
25
+ test('hasNoEffect is true when the window holds no event, DOM change, app request or URL change', () => {
26
+ expect(hasNoEffect({ window: windowWith({}) })).toBe(true);
27
+ });
28
+
29
+ test.each([
30
+ ['an event', { emits: [{ blockId: 'save', eventName: 'onClick', success: true }] }],
31
+ ['a DOM change', { mutationCount: 2 }],
32
+ ['a replaced document', { mutationCount: null }],
33
+ ['an app request', { requests: [{ url: 'http://localhost:3227/api/request/orders/load' }] }],
34
+ ['a URL change', { urlAfter: 'http://localhost:3227/second' }],
35
+ ])('hasNoEffect is false when the window holds %s', (_, overrides) => {
36
+ expect(hasNoEffect({ window: windowWith(overrides) })).toBe(false);
37
+ });
@@ -0,0 +1,72 @@
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
+ // Serialized into the journey's page by page.evaluate, so this function must be
18
+ // pure: it may reference nothing but `window`, `document` and its argument.
19
+ //
20
+ // Opens a step's window in the page. Installed once per document (a
21
+ // full page load replaces it, so each step installs it again if needed): it
22
+ // subscribes to the engine's trace registry and keeps each completed event's
23
+ // value-free summary, and records the nodes DOM mutations added or changed
24
+ // under document.body. Only what happens while a window is open is kept.
25
+ // Throws when the page has no trace registry: a step's window never guesses.
26
+ function installStepObserver() {
27
+ const lowdefy = window.lowdefy;
28
+ if (!lowdefy || !lowdefy._trace || typeof lowdefy._trace.subscribe !== 'function') {
29
+ throw new Error(
30
+ 'The page has no Lowdefy trace registry (window.lowdefy._trace), so its events cannot be observed. Restart the dev server so pages load the current client.'
31
+ );
32
+ }
33
+ let observer = window.__lowdefyStepObserver;
34
+ if (!observer) {
35
+ observer = { open: false, emits: [], nodes: [] };
36
+ lowdefy._trace.subscribe((payload) => {
37
+ if (!observer.open) return;
38
+ observer.emits.push({
39
+ scope: payload.scope ?? null,
40
+ pageId: payload.pageId ?? null,
41
+ blockId: payload.blockId ?? null,
42
+ blockType: payload.blockType ?? null,
43
+ eventName: payload.eventName ?? null,
44
+ success: payload.success,
45
+ failure: payload.failure ?? null,
46
+ });
47
+ });
48
+ const mutations = new MutationObserver((records) => {
49
+ if (!observer.open) return;
50
+ records.forEach((record) => {
51
+ if (record.type === 'childList') {
52
+ record.addedNodes.forEach((node) => observer.nodes.push(node));
53
+ return;
54
+ }
55
+ observer.nodes.push(record.target);
56
+ });
57
+ });
58
+ mutations.observe(document.body, {
59
+ childList: true,
60
+ attributes: true,
61
+ characterData: true,
62
+ subtree: true,
63
+ });
64
+ window.__lowdefyStepObserver = observer;
65
+ }
66
+ observer.emits = [];
67
+ observer.nodes = [];
68
+ observer.open = true;
69
+ return true;
70
+ }
71
+
72
+ export default installStepObserver;
@@ -0,0 +1,33 @@
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
+ const APP_API_PREFIXES = ['/api/request/', '/api/endpoints/'];
18
+
19
+ // Whether a URL is one of the app's request or endpoint API routes on this
20
+ // dev server: what a journey step counts as the app doing work, and where a 5xx
21
+ // is a failed request.
22
+ function isAppApiUrl({ url, origin, basePath = '' }) {
23
+ let parsed;
24
+ try {
25
+ parsed = new URL(url);
26
+ } catch {
27
+ return false;
28
+ }
29
+ if (parsed.origin !== origin) return false;
30
+ return APP_API_PREFIXES.some((prefix) => parsed.pathname.startsWith(`${basePath}${prefix}`));
31
+ }
32
+
33
+ export default isAppApiUrl;