@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.
Files changed (624) 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 +78 -0
  99. package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
  100. package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +727 -0
  101. package/lib/docs/dataSets/describeDataSetResult.js +25 -0
  102. package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
  103. package/lib/docs/dataSets/describeTargetKey.js +29 -0
  104. package/lib/docs/dataSets/deserializeFixture.js +34 -0
  105. package/lib/docs/dataSets/drainSessionWork.js +37 -0
  106. package/lib/docs/dataSets/drainSessionWork.test.mjs +64 -0
  107. package/lib/docs/dataSets/duplicateIndexName.js +24 -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 +52 -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 +133 -0
  119. package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +493 -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 +83 -0
  126. package/lib/docs/dataSets/readDataSet.test.mjs +251 -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/sessionDatabaseName.js +32 -0
  133. package/lib/docs/dataSets/sweepOrphanDatabases.js +36 -0
  134. package/lib/docs/dataSets/test/app/lowdefy.yaml +225 -0
  135. package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
  136. package/lib/docs/dataSets/test/app/tests/data/shop.yaml +12 -0
  137. package/lib/docs/dataSets/test/buildTestApp.mjs +43 -0
  138. package/lib/docs/devEventBus.js +136 -0
  139. package/lib/docs/devEventBus.test.mjs +269 -0
  140. package/lib/docs/devToolDefinitions.js +529 -0
  141. package/lib/docs/docSources.js +21 -0
  142. package/lib/docs/docs.test.mjs +171 -15
  143. package/lib/docs/docsIndex.test.mjs +377 -0
  144. package/lib/docs/evalOperator.js +5 -3
  145. package/lib/docs/evalOperatorHeadless.js +84 -12
  146. package/lib/docs/evalOperatorHeadless.test.mjs +7 -1
  147. package/lib/docs/evalOperatorInTab.js +2 -1
  148. package/lib/docs/findConfig.js +95 -49
  149. package/lib/docs/fitResponse.js +81 -0
  150. package/lib/docs/fitResponse.test.mjs +128 -0
  151. package/lib/docs/formatFeedback.js +26 -5
  152. package/lib/docs/formatFeedback.test.mjs +48 -0
  153. package/lib/docs/getAppMap.js +7 -1
  154. package/lib/docs/getBrowser.js +150 -80
  155. package/lib/docs/getBrowser.test.mjs +407 -16
  156. package/lib/docs/getBuildId.js +61 -0
  157. package/lib/docs/getBuildId.test.mjs +84 -0
  158. package/lib/docs/getBuildStatus.js +81 -7
  159. package/lib/docs/getBuildStatus.test.mjs +229 -0
  160. package/lib/docs/getBuildStatusAfterEdits.js +35 -0
  161. package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
  162. package/lib/docs/getCoreDoc.js +9 -1
  163. package/lib/docs/getDoc.js +100 -0
  164. package/lib/docs/getDocsIndex.js +152 -0
  165. package/lib/docs/getHazards.js +115 -0
  166. package/lib/docs/getHazards.test.mjs +175 -0
  167. package/lib/docs/getMailOutboxDirectory.js +26 -0
  168. package/lib/docs/getOverview.js +79 -13
  169. package/lib/docs/getOverviewBasePath.test.mjs +37 -0
  170. package/lib/docs/getPageBuildStatus.js +46 -0
  171. package/lib/docs/getPluginDoc.js +21 -23
  172. package/lib/docs/getSchema.js +33 -1
  173. package/lib/docs/getStaleStatus.js +36 -0
  174. package/lib/docs/getStaleStatus.test.mjs +74 -0
  175. package/lib/docs/guardJourneyOrigin.js +39 -0
  176. package/lib/docs/inspectState.js +5 -3
  177. package/lib/docs/inspectStateFromTab.js +2 -2
  178. package/lib/docs/inspectStateHeadless.js +89 -27
  179. package/lib/docs/inspectStateHeadless.test.mjs +20 -1
  180. package/lib/docs/installHeadlessShell.failure.test.mjs +45 -0
  181. package/lib/docs/installHeadlessShell.js +123 -0
  182. package/lib/docs/installHeadlessShell.test.mjs +70 -0
  183. package/lib/docs/installHeadlessShell.timeout.test.mjs +72 -0
  184. package/lib/docs/isHeadlessShellIncomplete.js +34 -0
  185. package/lib/docs/isHeadlessShellIncomplete.test.mjs +63 -0
  186. package/lib/docs/isPageReady.js +77 -0
  187. package/lib/docs/isPageReady.test.mjs +235 -0
  188. package/lib/docs/isRunErrorEntry.js +25 -0
  189. package/lib/docs/journeyMutantCookie.chromium.test.mjs +150 -0
  190. package/lib/docs/journeyRunRecording.js +35 -0
  191. package/lib/docs/journeyRunRecording.test.mjs +39 -0
  192. package/lib/docs/keepEventStreamAlive.js +68 -0
  193. package/lib/docs/keepEventStreamAlive.test.mjs +77 -0
  194. package/lib/docs/launchBrowser.js +83 -0
  195. package/lib/docs/launchBrowser.test.mjs +175 -0
  196. package/lib/docs/listModuleDocEntries.js +69 -0
  197. package/lib/docs/listPackageDocFiles.js +67 -0
  198. package/lib/docs/listPluginDocEntries.js +111 -0
  199. package/lib/docs/listPlugins.js +9 -58
  200. package/lib/docs/listTypes.js +14 -6
  201. package/lib/docs/loadFrameworkHazards.js +40 -0
  202. package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
  203. package/lib/docs/loadState.js +103 -35
  204. package/lib/docs/makeModuleManifestDoc.js +174 -0
  205. package/lib/docs/matchExercisedPages.js +35 -0
  206. package/lib/docs/matchJourneyRoute.js +69 -0
  207. package/lib/docs/matchJourneyRoute.test.mjs +178 -0
  208. package/lib/docs/mergeNetworkSnapshots.js +49 -0
  209. package/lib/docs/nextJourneySequence.js +30 -0
  210. package/lib/docs/noBrowserError.js +22 -0
  211. package/lib/docs/observe/TRANSIENT_SELECTOR.js +21 -0
  212. package/lib/docs/observe/claimExpectedErrors.js +67 -0
  213. package/lib/docs/observe/createJourneyAppErrors.js +236 -0
  214. package/lib/docs/observe/createJourneyAppErrors.test.mjs +385 -0
  215. package/lib/docs/observe/evaluateInvariants.js +183 -0
  216. package/lib/docs/observe/evaluateInvariants.test.mjs +299 -0
  217. package/lib/docs/observe/findingKey.js +29 -0
  218. package/lib/docs/observe/hasNoEffect.js +31 -0
  219. package/lib/docs/observe/hasNoEffect.test.mjs +37 -0
  220. package/lib/docs/observe/installStepObserver.js +72 -0
  221. package/lib/docs/observe/isAppApiUrl.js +33 -0
  222. package/lib/docs/observe/parseAppApiUrl.js +37 -0
  223. package/lib/docs/observe/readStepObserver.js +42 -0
  224. package/lib/docs/observe/relativeSource.js +29 -0
  225. package/lib/docs/observe/resolveConfigKeySource.js +33 -0
  226. package/lib/docs/observe/takeJourneyEvents.js +36 -0
  227. package/lib/docs/observe/toAppErrorFailure.js +61 -0
  228. package/lib/docs/observe/usesSearchStage.js +45 -0
  229. package/lib/docs/observe/waitForClientErrorReports.js +31 -0
  230. package/lib/docs/observe/watchJourneyContext.js +75 -0
  231. package/lib/docs/observe/withAppErrors.js +31 -0
  232. package/lib/docs/observe/withAppErrors.test.mjs +101 -0
  233. package/lib/docs/openJourney.js +94 -0
  234. package/lib/docs/openJourneyEmail.js +41 -0
  235. package/lib/docs/openJourneyEmail.test.mjs +187 -0
  236. package/lib/docs/pageBlocks.js +47 -0
  237. package/lib/docs/pageBuildStatus.test.mjs +103 -0
  238. package/lib/{client/setPageId.js → docs/parseModuleSource.js} +11 -12
  239. package/lib/docs/rails/authEngineActions.js +53 -0
  240. package/lib/docs/rails/authEngineActions.test.mjs +69 -0
  241. package/lib/docs/rails/authEngineMethods.js +52 -0
  242. package/lib/docs/rails/authEngineMethods.test.mjs +36 -0
  243. package/lib/docs/rails/checkAuthActionsRan.js +66 -0
  244. package/lib/docs/rails/checkAuthActionsRan.test.mjs +149 -0
  245. package/lib/docs/rails/checkClickRails.js +56 -0
  246. package/lib/docs/rails/createDataSetRails.js +82 -0
  247. package/lib/docs/rails/createDataSetRails.test.mjs +172 -0
  248. package/lib/docs/rails/findExternalBlocks.js +147 -0
  249. package/lib/docs/rails/findExternalBlocks.test.mjs +254 -0
  250. package/lib/docs/rails/installAuthActionRecorder.js +56 -0
  251. package/lib/docs/rails/readAuthActionRecorder.js +32 -0
  252. package/lib/docs/rails/readBuiltArtifact.js +30 -0
  253. package/lib/docs/readConnectionPlugins.js +26 -0
  254. package/lib/docs/readDevAuthMode.js +32 -0
  255. package/lib/docs/readDocEntry.js +40 -0
  256. package/lib/docs/readDocTitle.js +26 -0
  257. package/lib/docs/readJourneyEmailMatch.js +57 -0
  258. package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
  259. package/lib/docs/readJourneySession.js +56 -0
  260. package/lib/docs/readMailOutbox.js +37 -0
  261. package/lib/docs/readPagePath.js +27 -0
  262. package/lib/docs/readPluginPackages.js +99 -0
  263. package/lib/docs/readProxyBuildWait.js +38 -0
  264. package/lib/docs/readRoutineSteps.js +45 -0
  265. package/lib/docs/registerShownTextEngine.js +118 -0
  266. package/lib/docs/requestRestart.js +30 -0
  267. package/lib/docs/requestRestart.test.mjs +49 -0
  268. package/lib/docs/resolvePageInstance.js +46 -0
  269. package/lib/docs/resolvePageInstance.test.mjs +81 -0
  270. package/lib/docs/resolveSource.js +11 -7
  271. package/lib/docs/resolveSource.test.mjs +1 -1
  272. package/lib/docs/resolveToolCaller.js +62 -0
  273. package/lib/docs/reviewPage.js +51 -0
  274. package/lib/docs/reviewPageBuilds.js +56 -0
  275. package/lib/docs/reviewPageBuilds.test.mjs +207 -0
  276. package/lib/docs/runCheck.js +75 -0
  277. package/lib/docs/runCheck.test.mjs +91 -0
  278. package/lib/docs/runCheckChild.js +49 -0
  279. package/lib/docs/runEndpoint.js +185 -0
  280. package/lib/docs/runEndpoint.test.mjs +509 -0
  281. package/lib/docs/runErrorBuffers.js +99 -0
  282. package/lib/docs/runErrorBuffers.test.mjs +95 -0
  283. package/lib/docs/runJourney.js +347 -0
  284. package/lib/docs/runJourney.test.mjs +2383 -0
  285. package/lib/docs/runJourneyAsUsers.js +44 -0
  286. package/lib/docs/runJourneyDataSet.test.mjs +274 -0
  287. package/lib/docs/runJourneySteps.js +1320 -0
  288. package/lib/docs/runRequest.js +100 -59
  289. package/lib/docs/runRequest.test.mjs +190 -0
  290. package/lib/docs/saveAnnotatedScreenshot.js +1 -1
  291. package/lib/docs/scaffoldPage.js +3 -1
  292. package/lib/docs/screenshotPage.js +163 -11
  293. package/lib/docs/screenshotPage.test.mjs +263 -1
  294. package/lib/docs/screenshotPageClip.test.mjs +4 -0
  295. package/lib/docs/searchDocs.js +82 -30
  296. package/lib/docs/searchDocs.test.mjs +100 -0
  297. package/lib/docs/searchIcons.js +90 -0
  298. package/lib/docs/searchIcons.test.mjs +127 -0
  299. package/lib/docs/selectExercised.js +39 -0
  300. package/lib/docs/selectFinalState.js +66 -0
  301. package/lib/docs/selectFinalState.test.mjs +51 -0
  302. package/lib/docs/serverErrorStore.js +54 -0
  303. package/lib/docs/serverErrorStore.test.mjs +50 -0
  304. package/lib/docs/setupTestFixtures.mjs +133 -39
  305. package/lib/docs/shownTextSelector.js +28 -0
  306. package/lib/docs/snapshotState.js +13 -5
  307. package/lib/docs/splitSearchTerms.js +62 -0
  308. package/lib/docs/splitSearchTerms.test.mjs +50 -0
  309. package/lib/docs/stateCheckpoints.test.mjs +77 -0
  310. package/lib/docs/tabAvailable.js +6 -5
  311. package/lib/docs/tabChannel.js +127 -21
  312. package/lib/docs/tabChannel.test.mjs +187 -29
  313. package/lib/docs/unsettledPageNote.js +24 -0
  314. package/lib/docs/validateJourneyMail.js +36 -0
  315. package/lib/docs/validateJourneyTimeout.js +38 -0
  316. package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
  317. package/lib/docs/validateRecordingParam.js +42 -0
  318. package/lib/docs/validateStateSelection.js +33 -0
  319. package/lib/docs/validateStateSelection.test.mjs +33 -0
  320. package/lib/docs/validateViewport.js +51 -0
  321. package/lib/docs/validateViewport.test.mjs +54 -0
  322. package/lib/docs/waitForBuild.js +50 -0
  323. package/lib/docs/waitForBuild.test.mjs +75 -0
  324. package/lib/docs/waitForJourneyEmail.js +77 -0
  325. package/lib/docs/withBrowserSlot.js +45 -0
  326. package/lib/docs/withBrowserSlot.test.mjs +75 -0
  327. package/lib/docs/withDataSession.js +53 -0
  328. package/lib/server/applyDataSetRedirect.js +92 -0
  329. package/lib/server/applyDataSetRedirect.test.mjs +446 -0
  330. package/lib/server/applyDataSetRedirectTenantReach.test.mjs +207 -0
  331. package/lib/server/applyDataSetRedirectWalled.test.mjs +134 -0
  332. package/lib/server/auth/createSystemContext.js +73 -0
  333. package/lib/server/auth/{getAuthConfig.js → getAuth.js} +15 -9
  334. package/lib/server/auth/getHeadlessUser.js +9 -8
  335. package/lib/server/auth/getHeadlessUser.test.mjs +41 -0
  336. package/lib/server/auth/getMockUser.js +18 -14
  337. package/lib/server/auth/{session.js → getStrategies.js} +17 -16
  338. package/lib/server/auth/headlessUser.js +3 -2
  339. package/lib/server/auth/journeyActor.js +35 -0
  340. package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
  341. package/lib/server/checkPageRecord.js +44 -0
  342. package/lib/server/contextMapBudget.js +24 -0
  343. package/lib/server/contextMapBudget.test.mjs +98 -0
  344. package/lib/server/continueJitKeys.js +28 -0
  345. package/lib/server/continueJitKeys.test.mjs +43 -0
  346. package/lib/server/createDataSessionEndedError.js +33 -0
  347. package/lib/server/createDataSetRefusal.js +26 -0
  348. package/lib/server/createLowdefyContext.js +168 -41
  349. package/lib/server/createLowdefyContext.test.mjs +331 -8
  350. package/lib/server/getClientAddress.js +51 -0
  351. package/lib/server/getClientAddress.test.mjs +56 -0
  352. package/lib/server/getPageJitEnrichment.test.mjs +29 -11
  353. package/lib/server/hashConfigContent.js +26 -0
  354. package/lib/server/importAppCode.test.mjs +57 -0
  355. package/lib/server/isRoutineStep.js +27 -0
  356. package/lib/server/jitPageBuilder.js +309 -126
  357. package/lib/server/jitPageBuilder.skipPluginChecks.test.mjs +78 -0
  358. package/lib/server/jitPageBuilder.test.mjs +218 -0
  359. package/lib/server/journeyCookies/JOURNEY_COOKIES.js +37 -0
  360. package/lib/server/journeyCookies/forwardJourneyCookies.js +46 -0
  361. package/lib/server/journeyCookies/readJourneyCookie.js +52 -0
  362. package/lib/server/journeyCookies/writeJourneyCookie.js +46 -0
  363. package/lib/server/journeyCookies.js +22 -0
  364. package/lib/server/journeyCookies.test.mjs +145 -0
  365. package/lib/server/loadDynamicJsMap.js +47 -0
  366. package/lib/server/log/createHandleError.js +38 -2
  367. package/lib/server/log/createHandleError.test.mjs +125 -2
  368. package/lib/server/log/createLogger.js +3 -0
  369. package/lib/server/log/createLogger.test.mjs +15 -0
  370. package/lib/server/log/logRequest.js +2 -3
  371. package/lib/server/mutantDataSetWrappers.test.mjs +115 -0
  372. package/lib/server/mutants/applyMutant.js +39 -0
  373. package/lib/server/mutants/artifactPatterns.js +25 -0
  374. package/lib/server/mutants/buildAnchor.js +45 -0
  375. package/lib/server/mutants/collectSharedIds.js +41 -0
  376. package/lib/server/mutants/configPathSegments.js +38 -0
  377. package/lib/server/mutants/createMutantReadConfigFile.js +42 -0
  378. package/lib/server/mutants/describeEvent.js +22 -0
  379. package/lib/server/mutants/enumerateArtifact.js +56 -0
  380. package/lib/server/mutants/findNodeByKey.js +45 -0
  381. package/lib/server/mutants/findNodeByPointer.js +49 -0
  382. package/lib/server/mutants/getArtifactKind.js +32 -0
  383. package/lib/server/mutants/groupMutantCopies.js +56 -0
  384. package/lib/server/mutants/hashArtifact.js +25 -0
  385. package/lib/server/mutants/hashMutantTarget.js +34 -0
  386. package/lib/server/mutants/listMutants.js +122 -0
  387. package/lib/server/mutants/listMutants.test.mjs +502 -0
  388. package/lib/server/mutants/mutantId.js +30 -0
  389. package/lib/server/mutants/mutantRuns.js +57 -0
  390. package/lib/server/mutants/mutantRuns.test.mjs +136 -0
  391. package/lib/server/mutants/operators/dropAction.js +47 -0
  392. package/lib/server/mutants/operators/dropBlock.js +44 -0
  393. package/lib/server/mutants/operators/dropPayload.js +52 -0
  394. package/lib/server/mutants/operators/dropStep.js +44 -0
  395. package/lib/server/mutants/operators/flipVisible.js +53 -0
  396. package/lib/server/mutants/operators/index.js +37 -0
  397. package/lib/server/mutants/operators/retargetLink.js +72 -0
  398. package/lib/server/mutants/operators/skipValidate.js +77 -0
  399. package/lib/server/mutants/operators/swapIf.js +112 -0
  400. package/lib/server/mutants/operators.test.mjs +396 -0
  401. package/lib/server/mutants/positionFreePath.js +38 -0
  402. package/lib/server/mutants/removeFromParent.js +28 -0
  403. package/lib/server/mutants/test/fixtures.mjs +173 -0
  404. package/lib/server/mutants/test/keyArtifact.mjs +63 -0
  405. package/lib/server/mutants/validateMutantParam.js +69 -0
  406. package/lib/server/mutants/validateMutantsBody.js +88 -0
  407. package/lib/server/mutants/walkArtifact.js +179 -0
  408. package/lib/server/pageBuildRecords.js +131 -0
  409. package/lib/server/pageBuildRecords.test.mjs +258 -0
  410. package/lib/server/pageCache.mjs +22 -7
  411. package/lib/server/pageCache.test.mjs +22 -7
  412. package/lib/server/pageRebuilds.test.mjs +543 -0
  413. package/lib/server/pruneJitMaps.js +36 -0
  414. package/lib/server/pruneJitMaps.test.mjs +55 -0
  415. package/lib/server/readInstanceRecord.js +33 -0
  416. package/lib/server/readInstanceRecord.test.mjs +99 -0
  417. package/lib/server/readMergedMaps.js +86 -0
  418. package/lib/server/readMergedMaps.test.mjs +79 -0
  419. package/lib/server/recording/appendRecords.js +33 -0
  420. package/lib/server/recording/errorRecordingStamp.js +34 -0
  421. package/lib/server/recording/errorRecordingStamp.test.mjs +31 -0
  422. package/lib/server/recording/isRecordingEnabled.js +31 -0
  423. package/lib/server/recording/readRecordingCookie.js +71 -0
  424. package/lib/server/recording/readRecordingCookie.test.mjs +73 -0
  425. package/lib/server/recording/recordingCookiePayload.js +33 -0
  426. package/lib/server/recording/servedBuilds.js +46 -0
  427. package/lib/server/recording/servedBuilds.test.mjs +42 -0
  428. package/lib/server/sinkExternalConnections.js +95 -0
  429. package/lib/server/sinkExternalConnections.test.mjs +135 -0
  430. package/lib/server/skipStaleMapWrites.js +62 -0
  431. package/lib/server/skipStaleMapWrites.test.mjs +113 -0
  432. package/lib/server/startIdleGc.js +67 -0
  433. package/lib/server/startIdleGc.test.mjs +138 -0
  434. package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
  435. package/lib/server/trackSessionWork.js +30 -0
  436. package/lib/vite/createDevServerExclude.js +40 -0
  437. package/lib/vite/createDevServerExclude.test.mjs +79 -0
  438. package/manager/getContext.mjs +25 -3
  439. package/manager/processes/initialBuild.mjs +17 -1
  440. package/manager/processes/initialBuild.test.mjs +104 -0
  441. package/manager/processes/lowdefyBuild.mjs +31 -7
  442. package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +72 -0
  443. package/manager/processes/lowdefyBuild.test.mjs +136 -1
  444. package/manager/processes/optimizeDependencies.mjs +59 -0
  445. package/manager/processes/restartServer.mjs +34 -6
  446. package/manager/processes/restartServer.test.mjs +162 -0
  447. package/manager/processes/shutdownServer.mjs +11 -0
  448. package/manager/processes/shutdownServer.test.mjs +45 -0
  449. package/manager/processes/startFirstServer.mjs +46 -0
  450. package/manager/processes/startFirstServer.test.mjs +326 -0
  451. package/manager/processes/startMailSink.mjs +135 -0
  452. package/manager/processes/startMailSink.test.mjs +146 -0
  453. package/manager/processes/startProxy.mjs +129 -12
  454. package/manager/processes/startProxy.test.mjs +366 -0
  455. package/manager/processes/startServer.mjs +47 -6
  456. package/manager/processes/startServer.test.mjs +155 -0
  457. package/manager/processes/startWatchers.mjs +6 -4
  458. package/manager/processes/syncServer.mjs +69 -0
  459. package/manager/processes/syncServer.test.mjs +141 -0
  460. package/manager/processes/waitForDevServer.mjs +48 -0
  461. package/manager/processes/waitForDevServer.test.mjs +112 -0
  462. package/manager/run.mjs +128 -43
  463. package/manager/utils/BatchChanges.mjs +21 -5
  464. package/manager/utils/BatchChanges.test.mjs +77 -0
  465. package/manager/utils/acquireDevInstance.mjs +99 -0
  466. package/manager/utils/acquireDevInstance.test.mjs +160 -0
  467. package/manager/utils/createBuildActivity.mjs +67 -0
  468. package/manager/utils/createBuildActivity.test.mjs +58 -0
  469. package/manager/utils/createChangeSignal.mjs +39 -0
  470. package/manager/utils/createChangeSignal.test.mjs +62 -0
  471. package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
  472. package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
  473. package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
  474. package/manager/utils/createDotPathIgnore.mjs +44 -0
  475. package/manager/utils/createDotPathIgnore.test.mjs +65 -0
  476. package/manager/utils/createIdleGc.mjs +51 -0
  477. package/manager/utils/createIdleGc.test.mjs +77 -0
  478. package/manager/utils/createRequestActivity.mjs +100 -0
  479. package/manager/utils/createRequestActivity.test.mjs +121 -0
  480. package/manager/utils/createServerArtifactTracker.mjs +83 -0
  481. package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
  482. package/manager/utils/createServerEnv.mjs +39 -0
  483. package/manager/utils/filesHaveSameBytes.mjs +41 -0
  484. package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
  485. package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
  486. package/manager/utils/importFresh.mjs +46 -0
  487. package/manager/utils/importFresh.test.mjs +72 -0
  488. package/manager/utils/importFreshWorker.mjs +20 -0
  489. package/manager/utils/killTaggedBrowser.mjs +35 -0
  490. package/manager/utils/pagePathChanged.mjs +56 -0
  491. package/manager/utils/pruneRecordings.mjs +85 -0
  492. package/manager/utils/pruneRecordings.test.mjs +117 -0
  493. package/manager/utils/publishBuildDirectory.mjs +121 -0
  494. package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
  495. package/manager/utils/publishBuildDirectory.test.mjs +262 -0
  496. package/manager/utils/readBasePath.mjs +27 -0
  497. package/manager/utils/readBasePath.test.mjs +43 -0
  498. package/manager/utils/readBuildStatusWait.mjs +67 -0
  499. package/manager/utils/readManagerVersion.mjs +30 -0
  500. package/manager/utils/readManagerVersion.test.mjs +35 -0
  501. package/manager/utils/readPluginDefinitions.mjs +36 -0
  502. package/manager/utils/resolveDevAuthUrl.mjs +58 -0
  503. package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
  504. package/manager/utils/resolvePorts.mjs +49 -0
  505. package/manager/utils/setupWatcher.mjs +19 -11
  506. package/manager/utils/startRecordingPruner.mjs +45 -0
  507. package/manager/utils/startRecordingPruner.test.mjs +66 -0
  508. package/manager/utils/waitForChildExit.mjs +53 -0
  509. package/manager/utils/waitForChildExit.test.mjs +70 -0
  510. package/manager/utils/waitForServer.mjs +44 -0
  511. package/manager/utils/writeBuildStatus.mjs +1 -1
  512. package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
  513. package/manager/watchers/envWatcher.mjs +9 -2
  514. package/manager/watchers/envWatcher.test.mjs +87 -0
  515. package/manager/watchers/listServerSidePackages.mjs +47 -0
  516. package/manager/watchers/listServerSidePackages.test.mjs +38 -0
  517. package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
  518. package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
  519. package/manager/watchers/pluginSourceWatcher.mjs +79 -0
  520. package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
  521. package/manager/watchers/restartRequestWatcher.mjs +71 -0
  522. package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
  523. package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
  524. package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
  525. package/manager/watchers/serverPackageWatcher.mjs +38 -0
  526. package/package.json +60 -47
  527. package/package.original.json +60 -47
  528. package/src/app.js +131 -20
  529. package/src/html/renderDevPage.js +63 -11
  530. package/src/html/renderDevPage.test.mjs +138 -0
  531. package/src/lib/getRequestPath.js +29 -0
  532. package/src/middleware/createSameOriginGuard.js +37 -0
  533. package/src/middleware/createSameOriginGuard.test.mjs +102 -0
  534. package/src/middleware/errorHandler.js +51 -1
  535. package/src/middleware/errorHandler.test.mjs +173 -3
  536. package/src/middleware/isRebindingSafeHost.js +44 -0
  537. package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
  538. package/src/middleware/localDevToolsOnly.js +38 -0
  539. package/src/middleware/localDevToolsOnly.test.mjs +61 -0
  540. package/src/middleware/noStore.js +27 -0
  541. package/src/middleware/noStore.test.mjs +48 -0
  542. package/src/middleware/requestTimeout.js +38 -0
  543. package/src/middleware/staleFlag.js +76 -0
  544. package/src/middleware/staleFlag.test.mjs +127 -0
  545. package/src/middleware/stampCaller.js +38 -0
  546. package/src/middleware/stampCaller.test.mjs +64 -0
  547. package/src/routes/agent.js +15 -3
  548. package/src/routes/auth.js +29 -20
  549. package/src/routes/auth.test.mjs +30 -56
  550. package/src/routes/clientError.js +13 -13
  551. package/src/routes/cron.js +3 -1
  552. package/src/routes/cronForward.js +3 -1
  553. package/src/routes/detached.js +22 -9
  554. package/src/routes/devInspect.js +17 -8
  555. package/src/routes/devInspect.test.mjs +44 -7
  556. package/src/routes/devRecording.js +113 -0
  557. package/src/routes/devRecording.test.mjs +253 -0
  558. package/src/routes/docs/buildStatus.js +10 -2
  559. package/src/routes/docs/buildStatus.test.mjs +46 -0
  560. package/src/routes/docs/content.js +5 -3
  561. package/src/routes/docs/evalOperator.js +15 -1
  562. package/src/routes/docs/events.js +50 -0
  563. package/src/routes/docs/events.test.mjs +117 -0
  564. package/src/routes/docs/find.js +9 -1
  565. package/src/routes/docs/icons.js +30 -0
  566. package/src/routes/docs/inspectState.js +16 -1
  567. package/src/routes/docs/journey.js +194 -0
  568. package/src/routes/docs/journey.test.mjs +401 -0
  569. package/src/routes/docs/journeyRefusals.test.mjs +71 -0
  570. package/src/routes/docs/loadState.js +4 -2
  571. package/src/routes/docs/mcp.js +34 -3
  572. package/src/routes/docs/mcp.test.mjs +130 -0
  573. package/src/routes/docs/mutants.js +78 -0
  574. package/src/routes/docs/mutants.test.mjs +180 -0
  575. package/src/routes/docs/parsePathParamsParam.js +41 -0
  576. package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
  577. package/src/routes/docs/parseUserParam.js +27 -21
  578. package/src/routes/docs/parseUserParam.test.mjs +17 -5
  579. package/src/routes/docs/restart.js +35 -0
  580. package/src/routes/docs/restart.test.mjs +64 -0
  581. package/src/routes/docs/runEndpoint.js +54 -0
  582. package/src/routes/docs/runEndpoint.test.mjs +165 -0
  583. package/src/routes/docs/runRequest.js +25 -8
  584. package/src/routes/docs/runRequest.test.mjs +92 -0
  585. package/src/routes/docs/screenshot.js +50 -2
  586. package/src/routes/docs/screenshot.test.mjs +117 -0
  587. package/src/routes/docs/search.js +15 -2
  588. package/src/routes/docs/snapshotState.js +17 -3
  589. package/src/routes/endpoints.js +27 -26
  590. package/src/routes/endpointsTextAnswer.test.mjs +77 -0
  591. package/src/routes/feedback.js +10 -13
  592. package/src/routes/jitPage.js +99 -35
  593. package/src/routes/jitPage.test.mjs +248 -0
  594. package/src/routes/mcp.js +82 -5
  595. package/src/routes/mcp.test.mjs +215 -0
  596. package/src/routes/mcpMemberToken.test.mjs +125 -0
  597. package/src/routes/mcpProtectedResourceMetadata.js +34 -0
  598. package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
  599. package/src/routes/mountDevPageRoutes.js +25 -0
  600. package/src/routes/mountDevPageRoutes.test.mjs +30 -0
  601. package/src/routes/mountOauthDiscovery.js +44 -0
  602. package/src/routes/mountOauthDiscovery.test.mjs +79 -0
  603. package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
  604. package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
  605. package/src/routes/reload.js +16 -0
  606. package/src/routes/request.js +6 -4
  607. package/src/routes/request.test.mjs +9 -5
  608. package/src/routes/requestBody.test.mjs +159 -0
  609. package/src/routes/usage.js +6 -2
  610. package/src/routes/user.js +27 -0
  611. package/src/routes/websocket.js +29 -8
  612. package/src/routes/websocket.test.mjs +65 -0
  613. package/src/routes/wellKnownFallback.js +30 -0
  614. package/src/routes/wellKnownFallback.test.mjs +32 -0
  615. package/src/websocket/devWebSocket.js +41 -26
  616. package/src/websocket/devWebSocket.test.mjs +157 -0
  617. package/vite.config.js +53 -23
  618. package/lib/server/auth/getDevSession.js +0 -70
  619. package/lib/server/auth/getDevSession.test.mjs +0 -138
  620. package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
  621. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
  622. package/manager/watchers/moduleBuildWatcher.mjs +0 -79
  623. package/manager/watchers/serverArtifactWatcher.mjs +0 -120
  624. package/src/routes/apiPage.js +0 -37
@@ -0,0 +1,502 @@
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 { serializer } from '@lowdefy/helpers';
18
+
19
+ import applyMutant from './applyMutant.js';
20
+ import hashArtifact from './hashArtifact.js';
21
+ import listMutants from './listMutants.js';
22
+ import mutantId from './mutantId.js';
23
+ import positionFreePath from './positionFreePath.js';
24
+ import collectSharedIds from './collectSharedIds.js';
25
+ import keyArtifact from './test/keyArtifact.mjs';
26
+ import { pageFixture } from './test/fixtures.mjs';
27
+
28
+ function reader(artifacts) {
29
+ return async (name) => artifacts[name] ?? null;
30
+ }
31
+
32
+ async function listPage({ prefix, value }) {
33
+ const { root, keyMap } = value ? keyArtifact({ value, prefix }) : pageFixture({ prefix });
34
+ return listMutants({
35
+ pages: ['tickets'],
36
+ readConfigFile: reader({ 'pages/tickets.json': root }),
37
+ keyMap,
38
+ refMap: {},
39
+ });
40
+ }
41
+
42
+ test('positionFreePath drops the index of every item with an id and keeps id-less items', () => {
43
+ expect(
44
+ positionFreePath({
45
+ configPath:
46
+ 'root.pages[0:tickets:PageHeaderMenu].blocks[3:assign_submit:Button].events.onClick[1:assign:Request]',
47
+ })
48
+ ).toEqual(
49
+ 'root.pages[tickets:PageHeaderMenu].blocks[assign_submit:Button].events.onClick[assign:Request]'
50
+ );
51
+ expect(positionFreePath({ configPath: 'root.blocks[2:name:TextInput].validate[0]' })).toEqual(
52
+ 'root.blocks[name:TextInput].validate[0]'
53
+ );
54
+ });
55
+
56
+ test('positionFreePath keeps the index of siblings that share an id', () => {
57
+ const keyMap = {
58
+ a: { key: 'root.blocks[0:box:Box].events.onClick[0:set:SetState]' },
59
+ b: { key: 'root.blocks[0:box:Box].events.onClick[1:set:SetState]' },
60
+ c: { key: 'root.blocks[0:box:Box]' },
61
+ };
62
+ const sharedIds = collectSharedIds({ keyMap });
63
+ expect(positionFreePath({ configPath: keyMap.a.key, sharedIds })).toEqual(
64
+ 'root.blocks[box:Box].events.onClick[0:set:SetState]'
65
+ );
66
+ expect(positionFreePath({ configPath: keyMap.b.key, sharedIds })).toEqual(
67
+ 'root.blocks[box:Box].events.onClick[1:set:SetState]'
68
+ );
69
+ });
70
+
71
+ test('mutantId is 12 hex characters and changes with the operator and arg', () => {
72
+ const base = {
73
+ artifact: 'pages/a.json',
74
+ configPath: 'root.blocks[x:Box]',
75
+ operator: 'drop-block',
76
+ };
77
+ expect(mutantId(base)).toMatch(/^[0-9a-f]{12}$/);
78
+ expect(mutantId({ ...base, operator: 'flip-visible', arg: 'visible' })).not.toEqual(
79
+ mutantId(base)
80
+ );
81
+ });
82
+
83
+ test('mutant ids are equal across two builds of unchanged config whose ~k prefixes differ', async () => {
84
+ const first = await listPage({ prefix: 'aaaa_1_' });
85
+ const second = await listPage({ prefix: 'bbbb_9_' });
86
+ expect(first.mutants.map((mutant) => mutant.key)).not.toEqual(
87
+ second.mutants.map((mutant) => mutant.key)
88
+ );
89
+ expect(first.mutants.map((mutant) => mutant.id)).toEqual(
90
+ second.mutants.map((mutant) => mutant.id)
91
+ );
92
+ expect(new Set(first.mutants.map((mutant) => mutant.id)).size).toEqual(first.mutants.length);
93
+ });
94
+
95
+ function simplePage(blocks) {
96
+ return {
97
+ id: 'page:tickets',
98
+ type: 'Box',
99
+ pageId: 'tickets',
100
+ blockId: 'tickets',
101
+ slots: { content: { blocks } },
102
+ };
103
+ }
104
+
105
+ test('a mutant keeps its id when a block is inserted above its node', async () => {
106
+ const before = await listPage({
107
+ prefix: 'p1_',
108
+ value: simplePage([{ id: 'block:tickets:save', blockId: 'save', type: 'Button' }]),
109
+ });
110
+ const after = await listPage({
111
+ prefix: 'p2_',
112
+ value: simplePage([
113
+ { id: 'block:tickets:intro', blockId: 'intro', type: 'Paragraph' },
114
+ { id: 'block:tickets:save', blockId: 'save', type: 'Button' },
115
+ ]),
116
+ });
117
+ const saveBefore = before.mutants.find((mutant) => mutant.describe.includes('"save"'));
118
+ const saveAfter = after.mutants.find((mutant) => mutant.describe.includes('"save"'));
119
+ expect(saveAfter.id).toEqual(saveBefore.id);
120
+ });
121
+
122
+ test('siblings sharing an id get distinct mutant ids', async () => {
123
+ const { mutants } = await listPage({
124
+ prefix: 's_',
125
+ value: simplePage([
126
+ {
127
+ id: 'block:tickets:save',
128
+ blockId: 'save',
129
+ type: 'Button',
130
+ events: {
131
+ onClick: {
132
+ try: [
133
+ { id: 'set', type: 'SetState', params: { a: 1 } },
134
+ { id: 'set', type: 'SetState', params: { b: 2 } },
135
+ ],
136
+ },
137
+ },
138
+ },
139
+ ]),
140
+ });
141
+ const ids = mutants
142
+ .filter((mutant) => mutant.operator === 'drop-action')
143
+ .map((mutant) => mutant.id);
144
+ expect(ids).toHaveLength(2);
145
+ expect(ids[0]).not.toEqual(ids[1]);
146
+ });
147
+
148
+ test('listMutants answers each mutant with its anchor, source, config and the artifact hash', async () => {
149
+ const { root, keyMap } = pageFixture();
150
+ Object.values(keyMap).forEach((entry) => {
151
+ entry['~r'] = 'pageRef';
152
+ entry['~l'] = 7;
153
+ });
154
+ const result = await listMutants({
155
+ pages: ['tickets'],
156
+ operators: ['drop-block'],
157
+ readConfigFile: reader({ 'pages/tickets.json': root }),
158
+ keyMap,
159
+ refMap: { pageRef: { path: 'pages/tickets.yaml' } },
160
+ });
161
+ expect(result.artifacts).toEqual({ 'pages/tickets.json': hashArtifact(root) });
162
+ expect(result.mutants[0]).toEqual({
163
+ id: expect.stringMatching(/^[0-9a-f]{12}$/),
164
+ operator: 'drop-block',
165
+ artifact: 'pages/tickets.json',
166
+ key: root.slots.content.blocks[0]['~k'],
167
+ arg: null,
168
+ anchor: { type: 'block', pageId: 'tickets', blockId: 'title', parentBlockId: 'tickets' },
169
+ source: 'pages/tickets.yaml:7',
170
+ config: 'root.slots.content.blocks[0:title:Title]',
171
+ describe: 'drop-block Title "title" from tickets',
172
+ copies: [],
173
+ copyTargets: [],
174
+ pointer: ['slots', 'content', 'blocks', 0],
175
+ nodeHash: expect.stringMatching(/^[0-9a-f]{40}$/),
176
+ });
177
+ });
178
+
179
+ test('source resolves through a module ref to the real file and line', async () => {
180
+ const { root, keyMap } = pageFixture();
181
+ Object.values(keyMap).forEach((entry) => {
182
+ entry['~r'] = 'moduleInvocation';
183
+ entry['~l'] = 42;
184
+ });
185
+ const { mutants } = await listMutants({
186
+ pages: ['tickets'],
187
+ operators: ['drop-block'],
188
+ readConfigFile: reader({ 'pages/tickets.json': root }),
189
+ keyMap,
190
+ refMap: {
191
+ moduleInvocation: { parent: 'moduleFile' },
192
+ moduleFile: { path: 'modules/tickets/pages/tickets.yaml', parent: null },
193
+ },
194
+ });
195
+ expect(mutants[0].source).toEqual('modules/tickets/pages/tickets.yaml:42');
196
+ });
197
+
198
+ function layoutPage({ pageId, footerText }) {
199
+ return {
200
+ id: `page:${pageId}`,
201
+ type: 'Box',
202
+ pageId,
203
+ blockId: pageId,
204
+ slots: {
205
+ content: {
206
+ blocks: [
207
+ {
208
+ id: `block:${pageId}:footer`,
209
+ blockId: 'footer',
210
+ type: 'Paragraph',
211
+ properties: { content: footerText },
212
+ },
213
+ ],
214
+ },
215
+ },
216
+ };
217
+ }
218
+
219
+ test("copies of one _ref'd node on three pages group into one mutant with two copies", async () => {
220
+ const artifacts = {};
221
+ const keyMap = {};
222
+ ['orders', 'contacts', 'tickets'].forEach((pageId, index) => {
223
+ const { root } = keyArtifact({
224
+ value: layoutPage({ pageId, footerText: 'Made with care' }),
225
+ prefix: `l${index}_`,
226
+ keyMap,
227
+ });
228
+ artifacts[`pages/${pageId}.json`] = root;
229
+ });
230
+ // The footer came from one layout file, at one line, on every page.
231
+ Object.values(keyMap).forEach((entry) => {
232
+ if (entry.key.endsWith('[0:footer:Paragraph]')) {
233
+ entry['~r'] = 'layout';
234
+ entry['~l'] = 3;
235
+ }
236
+ });
237
+ const { mutants } = await listMutants({
238
+ pages: ['tickets', 'orders', 'contacts'],
239
+ operators: ['drop-block'],
240
+ readConfigFile: reader(artifacts),
241
+ keyMap,
242
+ refMap: { layout: { path: 'layouts/footer.yaml' } },
243
+ });
244
+ expect(mutants).toHaveLength(1);
245
+ expect(mutants[0].anchor.pageId).toEqual('contacts');
246
+ expect(mutants[0].copies).toEqual(['orders', 'tickets']);
247
+ // Each copy carries its own page's key, so a journey on that page runs its copy.
248
+ expect(mutants[0].copyTargets).toEqual(
249
+ ['orders', 'tickets'].map((pageId) => ({
250
+ artifact: `pages/${pageId}.json`,
251
+ key: artifacts[`pages/${pageId}.json`].slots.content.blocks[0]['~k'],
252
+ pointer: ['slots', 'content', 'blocks', 0],
253
+ anchor: { type: 'block', pageId, blockId: 'footer', parentBlockId: pageId },
254
+ }))
255
+ );
256
+ expect(mutants[0].key).toBe(artifacts['pages/contacts.json'].slots.content.blocks[0]['~k']);
257
+ });
258
+
259
+ test('ids lists every copy of a grouped mutant, so a copy kept by an earlier listing is found', async () => {
260
+ const artifacts = {};
261
+ const keyMap = {};
262
+ ['orders', 'tickets'].forEach((pageId, index) => {
263
+ artifacts[`pages/${pageId}.json`] = keyArtifact({
264
+ value: layoutPage({ pageId, footerText: 'Made with care' }),
265
+ prefix: `i${index}_`,
266
+ keyMap,
267
+ }).root;
268
+ });
269
+ const both = await listMutants({
270
+ pages: ['orders', 'tickets'],
271
+ operators: ['drop-block'],
272
+ readConfigFile: reader(artifacts),
273
+ keyMap,
274
+ refMap: {},
275
+ });
276
+ const ticketsOnly = await listMutants({
277
+ pages: ['tickets'],
278
+ operators: ['drop-block'],
279
+ readConfigFile: reader(artifacts),
280
+ keyMap,
281
+ refMap: {},
282
+ });
283
+ const footer = (listing) => listing.mutants.find(({ describe }) => describe.includes('footer'));
284
+ expect(footer(both).id).not.toBe(footer(ticketsOnly).id);
285
+ expect(both.ids).toContain(footer(ticketsOnly).id);
286
+ expect(both.ids).toContain(footer(both).id);
287
+ });
288
+
289
+ test('two template copies with different content stay two mutants', async () => {
290
+ const artifacts = {};
291
+ const keyMap = {};
292
+ [
293
+ ['orders', 'Orders footer'],
294
+ ['tickets', 'Tickets footer'],
295
+ ].forEach(([pageId, footerText], index) => {
296
+ artifacts[`pages/${pageId}.json`] = keyArtifact({
297
+ value: layoutPage({ pageId, footerText }),
298
+ prefix: `t${index}_`,
299
+ keyMap,
300
+ }).root;
301
+ });
302
+ Object.values(keyMap).forEach((entry) => {
303
+ if (entry.key.endsWith('[0:footer:Paragraph]')) {
304
+ entry['~r'] = 'template';
305
+ entry['~l'] = 3;
306
+ }
307
+ });
308
+ const { mutants } = await listMutants({
309
+ pages: ['orders', 'tickets'],
310
+ operators: ['drop-block'],
311
+ readConfigFile: reader(artifacts),
312
+ keyMap,
313
+ refMap: { template: { path: 'templates/footer.yaml' } },
314
+ });
315
+ expect(mutants).toHaveLength(2);
316
+ });
317
+
318
+ test('hashArtifact is stable across a re-read and changes when the content changes', () => {
319
+ const { root } = pageFixture();
320
+ const reread = serializer.deserialize(serializer.serialize(root));
321
+ expect(hashArtifact(reread)).toEqual(hashArtifact(root));
322
+ expect(hashArtifact(pageFixture({ prefix: 'other_' }).root)).toEqual(hashArtifact(root));
323
+ reread.slots.content.blocks[0].properties.content = 'changed';
324
+ expect(hashArtifact(reread)).not.toEqual(hashArtifact(root));
325
+ });
326
+
327
+ test('listMutants reads requests, endpoints and app events and skips artifacts that are missing', async () => {
328
+ const { root } = pageFixture();
329
+ const result = await listMutants({
330
+ pages: ['tickets', 'missing'],
331
+ requests: [{ pageId: 'tickets', requestId: 'assign_ticket' }],
332
+ endpoints: ['notify'],
333
+ appEvents: true,
334
+ readConfigFile: reader({ 'pages/tickets.json': root }),
335
+ keyMap: {},
336
+ refMap: {},
337
+ });
338
+ expect(Object.keys(result.artifacts)).toEqual(['pages/tickets.json']);
339
+ });
340
+
341
+ function requestPage({ pageId, own }) {
342
+ return {
343
+ id: `page:${pageId}`,
344
+ type: 'Box',
345
+ pageId,
346
+ blockId: pageId,
347
+ requests: [
348
+ ...own.map((requestId) => ({
349
+ id: `request:${pageId}:${requestId}`,
350
+ requestId,
351
+ pageId,
352
+ payload: { filter: 'x' },
353
+ })),
354
+ {
355
+ id: `request:${pageId}:unread_count`,
356
+ requestId: 'unread_count',
357
+ pageId,
358
+ payload: { app_name: 'encircle' },
359
+ },
360
+ ],
361
+ };
362
+ }
363
+
364
+ const requestPages = [
365
+ ['activity', []],
366
+ ['lapsing', ['get_a']],
367
+ ['versions', ['get_a', 'get_b']],
368
+ ];
369
+
370
+ // The pages as one build keys them: the component's request sits at a
371
+ // different position on each page, and has the same source line on all.
372
+ function buildRequestPages({ prefix }) {
373
+ const artifacts = {};
374
+ const keyMap = {};
375
+ requestPages.forEach(([pageId, own], index) => {
376
+ artifacts[`pages/${pageId}.json`] = keyArtifact({
377
+ value: requestPage({ pageId, own }),
378
+ prefix: `${prefix}${index}_`,
379
+ keyMap,
380
+ }).root;
381
+ });
382
+ Object.values(keyMap).forEach((entry) => {
383
+ if (entry.key.includes(':unread_count')) {
384
+ entry['~r'] = 'component';
385
+ entry['~l'] = 5;
386
+ }
387
+ });
388
+ return { artifacts, keyMap };
389
+ }
390
+
391
+ async function listRequestPages({ artifacts, keyMap }) {
392
+ const { mutants } = await listMutants({
393
+ pages: requestPages.map(([pageId]) => pageId),
394
+ operators: ['drop-payload'],
395
+ readConfigFile: reader(artifacts),
396
+ keyMap,
397
+ refMap: { component: { path: 'components/unread-count-request.yaml' } },
398
+ });
399
+ return mutants.find(({ arg }) => arg === 'app_name');
400
+ }
401
+
402
+ function dropAppName({ root, target, shared }) {
403
+ return applyMutant({
404
+ root: serializer.copy(root),
405
+ mutant: {
406
+ operator: 'drop-payload',
407
+ arg: 'app_name',
408
+ key: target.key,
409
+ pointer: target.pointer,
410
+ nodeHash: shared.nodeHash,
411
+ },
412
+ });
413
+ }
414
+
415
+ test("a request a _ref'd component adds at a different position on each page lists a key and pointer per page", async () => {
416
+ const { artifacts, keyMap } = buildRequestPages({ prefix: 'a' });
417
+ const shared = await listRequestPages({ artifacts, keyMap });
418
+ expect(shared.copies).toEqual(['lapsing', 'versions']);
419
+ const targets = [{ artifact: shared.artifact, key: shared.key, pointer: shared.pointer }].concat(
420
+ shared.copyTargets
421
+ );
422
+ expect(targets.map(({ artifact }) => artifact)).toEqual([
423
+ 'pages/activity.json',
424
+ 'pages/lapsing.json',
425
+ 'pages/versions.json',
426
+ ]);
427
+ expect(targets.map(({ pointer }) => pointer)).toEqual([
428
+ ['requests', 0, 'payload'],
429
+ ['requests', 1, 'payload'],
430
+ ['requests', 2, 'payload'],
431
+ ]);
432
+ targets.forEach((target) => {
433
+ expect(dropAppName({ root: artifacts[target.artifact], target, shared })).toEqual({
434
+ applied: true,
435
+ });
436
+ });
437
+ });
438
+
439
+ test('a mutant listed before its pages were rebuilt, and so re-keyed, still applies on every page by pointer', async () => {
440
+ const listed = buildRequestPages({ prefix: 'a' });
441
+ const shared = await listRequestPages(listed);
442
+ // The dev server rebuilds a page when its build context is recreated and
443
+ // issues it new keys; the config, and so the artifact's shape, is unchanged.
444
+ const rebuilt = buildRequestPages({ prefix: 'b' });
445
+ const targets = [{ artifact: shared.artifact, key: shared.key, pointer: shared.pointer }].concat(
446
+ shared.copyTargets
447
+ );
448
+ targets.forEach((target) => {
449
+ const root = rebuilt.artifacts[target.artifact];
450
+ const copy = serializer.copy(root);
451
+ expect(
452
+ applyMutant({
453
+ root: copy,
454
+ mutant: { operator: 'drop-payload', arg: 'app_name', key: target.key },
455
+ })
456
+ ).toEqual({ applied: false, reason: 'key not found' });
457
+ const result = dropAppName({ root, target, shared });
458
+ expect(result).toEqual({ applied: true });
459
+ });
460
+ });
461
+
462
+ test('a pointer into content that changed since the listing is a miss, not another node', async () => {
463
+ const listed = buildRequestPages({ prefix: 'a' });
464
+ const shared = await listRequestPages(listed);
465
+ const rebuilt = buildRequestPages({ prefix: 'b' });
466
+ const root = rebuilt.artifacts['pages/lapsing.json'];
467
+ root.requests[1].payload.app_name = 'other';
468
+ const target = shared.copyTargets.find(({ artifact }) => artifact === 'pages/lapsing.json');
469
+ expect(dropAppName({ root, target, shared })).toEqual({
470
+ applied: false,
471
+ reason: 'key not found',
472
+ });
473
+ expect(root.requests[1].payload.app_name).toEqual('other');
474
+ });
475
+
476
+ test('a single-page mutant applies by its key, whatever its pointer says', async () => {
477
+ const { root, keyMap } = pageFixture({ prefix: 'k1_' });
478
+ const { mutants } = await listMutants({
479
+ pages: ['tickets'],
480
+ operators: ['drop-block'],
481
+ readConfigFile: reader({ 'pages/tickets.json': root }),
482
+ keyMap,
483
+ refMap: {},
484
+ });
485
+ expect(mutants.length).toBeGreaterThan(0);
486
+ mutants.forEach((mutant) => {
487
+ expect(mutant.copyTargets).toEqual([]);
488
+ const copy = serializer.copy(root);
489
+ expect(
490
+ applyMutant({
491
+ root: copy,
492
+ mutant: {
493
+ operator: mutant.operator,
494
+ arg: mutant.arg,
495
+ key: mutant.key,
496
+ pointer: ['no', 'such', 'node'],
497
+ nodeHash: 'unrelated',
498
+ },
499
+ })
500
+ ).toEqual({ applied: true });
501
+ });
502
+ });
@@ -0,0 +1,30 @@
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
+ // A mutant's id, the same in every build of unchanged config: the ~k a mutant
20
+ // is applied by changes with every build, so sampling, reruns and reports use
21
+ // this instead. configPath is position-free (positionFreePath).
22
+ function mutantId({ artifact, configPath, operator, arg }) {
23
+ return crypto
24
+ .createHash('sha1')
25
+ .update(JSON.stringify([artifact, configPath, operator, arg ?? null]))
26
+ .digest('hex')
27
+ .slice(0, 12);
28
+ }
29
+
30
+ export default mutantId;
@@ -0,0 +1,57 @@
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
+ import { type } from '@lowdefy/helpers';
19
+
20
+ import { JOURNEY_COOKIES, readJourneyCookie } from '../journeyCookies.js';
21
+
22
+ // The open mutant runs, one per journey run that carries a mutant. Kept on
23
+ // globalThis because the journey route that opens a run and the API context
24
+ // that reads it may load as separate module instances in one process (Vite's
25
+ // SSR module graph and Node's), like the journey actor token.
26
+ const REGISTRY_KEY = Symbol.for('lowdefy.devServer.mutantRuns');
27
+ globalThis[REGISTRY_KEY] ??= new Map();
28
+ const registry = globalThis[REGISTRY_KEY];
29
+
30
+ // Registers a run for one mutant. The cookie payload is the run id: every
31
+ // request a journey's browser contexts send carries it, and the dev server
32
+ // serves those requests the mutated artifact. close() ends the run.
33
+ function openMutantRun({ mutant }) {
34
+ const id = crypto.randomBytes(16).toString('hex');
35
+ const run = { id, mutant, applied: 0, misses: [] };
36
+ registry.set(id, run);
37
+ return {
38
+ id,
39
+ run,
40
+ cookiePayload: id,
41
+ close: () => {
42
+ registry.delete(id);
43
+ },
44
+ };
45
+ }
46
+
47
+ // The open run a request's verified mutant cookie names, or null: no cookie, a
48
+ // forged token, or a run that has closed.
49
+ function readMutantRun(cookieHeader) {
50
+ const id = readJourneyCookie({ cookieHeader, name: JOURNEY_COOKIES.mutant.name });
51
+ if (type.isNone(id)) {
52
+ return null;
53
+ }
54
+ return registry.get(id) ?? null;
55
+ }
56
+
57
+ export { openMutantRun, readMutantRun };