@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
@@ -14,11 +14,19 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import { EventEmitter } from 'node:events';
17
18
  import fs from 'fs';
18
19
  import os from 'os';
19
20
  import path from 'path';
21
+ import { Hono } from 'hono';
20
22
  import { jest } from '@jest/globals';
21
23
 
24
+ const mockLaunch = jest.fn();
25
+ jest.unstable_mockModule('playwright-core', () => ({
26
+ chromium: { launch: mockLaunch },
27
+ selectors: { register: jest.fn(async () => {}) },
28
+ }));
29
+
22
30
  // openPage takes its browser as a parameter, so a fake one covers the cookie
23
31
  // injection without a real Chromium. lib/build/config.js reads build/config.json
24
32
  // from process.cwd() at import time — chdir into a fixture that has one before
@@ -29,49 +37,183 @@ fs.mkdirSync(path.join(fixtureDir, 'build'), { recursive: true });
29
37
  fs.writeFileSync(path.join(fixtureDir, 'build', 'config.json'), JSON.stringify({ basePath: '' }));
30
38
  process.chdir(fixtureDir);
31
39
 
32
- const { openPage } = await import('./getBrowser.js');
40
+ const { getBrowser, openPage, buildPageUrl } = await import('./getBrowser.js');
41
+ const { default: isPageReady } = await import('./isPageReady.js');
42
+ const { default: getClientAddress } = await import('../server/getClientAddress.js');
43
+ const { default: readRecordingCookie } = await import('../server/recording/readRecordingCookie.js');
33
44
 
34
45
  afterAll(() => {
35
46
  process.chdir(originalCwd);
36
47
  fs.rmSync(fixtureDir, { recursive: true, force: true });
37
48
  });
38
49
 
50
+ // Every caller closes the contexts openPage gives it; the tests do too, so the
51
+ // module's open-context count starts each test at 0.
52
+ const openedContexts = [];
53
+
54
+ afterEach(async () => {
55
+ await Promise.all(openedContexts.splice(0).map((context) => context.close()));
56
+ });
57
+
39
58
  function createBrowser() {
40
59
  const addCookies = jest.fn();
41
60
  const page = {
42
61
  goto: jest.fn().mockResolvedValue(undefined),
43
62
  waitForFunction: jest.fn().mockResolvedValue(undefined),
44
63
  };
45
- const context = {
64
+ // Like Playwright's, the context emits close once, when it first closes.
65
+ const context = Object.assign(new EventEmitter(), {
46
66
  addCookies,
47
67
  newPage: jest.fn().mockResolvedValue(page),
48
- };
68
+ route: jest.fn().mockResolvedValue(undefined),
69
+ });
70
+ let closed = false;
71
+ context.close = jest.fn(async () => {
72
+ if (closed) return;
73
+ closed = true;
74
+ context.emit('close');
75
+ });
76
+ openedContexts.push(context);
49
77
  return {
50
78
  browser: { newContext: jest.fn().mockResolvedValue(context) },
51
79
  addCookies,
52
80
  context,
81
+ page,
53
82
  };
54
83
  }
55
84
 
56
- function decodeUserCookie(addCookies) {
85
+ function decodeCallerCookie(addCookies) {
57
86
  const [[[cookie]]] = addCookies.mock.calls;
58
87
  return JSON.parse(Buffer.from(cookie.value, 'base64').toString());
59
88
  }
60
89
 
61
- test('openPage injects the default roleless user when no user is given', async () => {
90
+ function decodeUserCookie(addCookies) {
91
+ return decodeCallerCookie(addCookies).user;
92
+ }
93
+
94
+ test('openPage opens a 1280x800 light viewport by default', async () => {
95
+ const { browser } = createBrowser();
96
+
97
+ await openPage({ browser, origin: 'http://localhost:3001', pageId: 'home' });
98
+
99
+ expect(browser.newContext).toHaveBeenCalledWith({
100
+ viewport: { width: 1280, height: 800 },
101
+ colorScheme: 'light',
102
+ });
103
+ });
104
+
105
+ test('openPage opens the viewport size and colour scheme it is given', async () => {
106
+ const { browser } = createBrowser();
107
+
108
+ await openPage({
109
+ browser,
110
+ origin: 'http://localhost:3001',
111
+ pageId: 'home',
112
+ width: 390,
113
+ height: 844,
114
+ colorScheme: 'dark',
115
+ });
116
+
117
+ expect(browser.newContext).toHaveBeenCalledWith({
118
+ viewport: { width: 390, height: 844 },
119
+ colorScheme: 'dark',
120
+ });
121
+ });
122
+
123
+ test('openPage injects the default roleless user when no user is given, not marked explicit', async () => {
62
124
  const { browser, addCookies } = createBrowser();
63
125
 
64
126
  await openPage({ browser, origin: 'http://localhost:3001', pageId: 'home' });
65
127
 
66
- expect(decodeUserCookie(addCookies)).toEqual({
67
- sub: 'lowdefy-headless',
68
- id: 'lowdefy-headless',
69
- name: 'Lowdefy Headless',
70
- roles: [],
128
+ expect(decodeCallerCookie(addCookies)).toEqual({
129
+ user: { id: 'lowdefy-headless', name: 'Lowdefy Headless', roles: [] },
130
+ explicit: false,
71
131
  });
72
132
  });
73
133
 
74
- test('openPage injects a per-call user with roles', async () => {
134
+ test('openPage injects no user for user none, so the app resolves its own sessions', async () => {
135
+ const { browser, addCookies } = createBrowser();
136
+
137
+ const opened = await openPage({
138
+ browser,
139
+ origin: 'http://localhost:3001',
140
+ pageId: 'login',
141
+ user: 'none',
142
+ });
143
+
144
+ const names = addCookies.mock.calls.map(([[cookie]]) => cookie.name);
145
+ expect(names).toEqual(['lowdefy_recording']);
146
+ expect(opened.ready).toBe(true);
147
+ });
148
+
149
+ function findCookie(addCookies, name) {
150
+ return addCookies.mock.calls.map(([[cookie]]) => cookie).find((cookie) => cookie.name === name);
151
+ }
152
+
153
+ test('openPage marks a context with no recording as off, so screenshots and inspection never record', async () => {
154
+ const { browser, addCookies } = createBrowser();
155
+
156
+ await openPage({ browser, origin: 'http://localhost:3001', pageId: 'home' });
157
+
158
+ const cookie = findCookie(addCookies, 'lowdefy_recording');
159
+ expect(cookie).toMatchObject({ url: 'http://localhost:3001', httpOnly: true, sameSite: 'Lax' });
160
+ expect(readRecordingCookie(`lowdefy_recording=${cookie.value}`)).toBe('off');
161
+ });
162
+
163
+ test('openPage marks a recording context with a cookie readRecordingCookie verifies', async () => {
164
+ const { browser, addCookies, page } = createBrowser();
165
+ const recording = {
166
+ source: 'journey',
167
+ run: {
168
+ id: '20261003T151200Z-p0d4rm',
169
+ by: 'test',
170
+ journey: 'tests/journeys/tickets.yaml#Assign a ticket',
171
+ actor: 'main',
172
+ },
173
+ };
174
+
175
+ await openPage({ browser, origin: 'http://localhost:3001', pageId: 'home', recording });
176
+
177
+ const cookie = findCookie(addCookies, 'lowdefy_recording');
178
+ expect(readRecordingCookie(`lowdefy_recording=${cookie.value}`)).toEqual(recording);
179
+ const recordingCall = addCookies.mock.invocationCallOrder[addCookies.mock.calls.length - 1];
180
+ expect(recordingCall).toBeLessThan(page.goto.mock.invocationCallOrder[0]);
181
+ });
182
+
183
+ test('openPage gives the context the client address it is given, which the dev server resolves', async () => {
184
+ const { browser, addCookies } = createBrowser();
185
+
186
+ await openPage({
187
+ browser,
188
+ origin: 'http://localhost:3001',
189
+ pageId: 'login',
190
+ user: 'none',
191
+ clientAddress: '203.0.113.7',
192
+ });
193
+
194
+ expect(browser.newContext).toHaveBeenCalledWith({
195
+ viewport: { width: 1280, height: 800 },
196
+ colorScheme: 'light',
197
+ });
198
+ const [[[cookie]]] = addCookies.mock.calls;
199
+ expect(cookie).toMatchObject({
200
+ name: 'lowdefy_journey_actor',
201
+ url: 'http://localhost:3001',
202
+ httpOnly: true,
203
+ sameSite: 'Lax',
204
+ });
205
+ const app = new Hono();
206
+ app.get('/', (c) => c.text(getClientAddress(c)));
207
+ const env = { incoming: { socket: { remoteAddress: '127.0.0.1' } } };
208
+ const response = await app.request(
209
+ '/',
210
+ { headers: { cookie: `${cookie.name}=${cookie.value}` } },
211
+ env
212
+ );
213
+ expect(await response.text()).toBe('203.0.113.7');
214
+ });
215
+
216
+ test('openPage injects a per-call user with roles, marked explicit so it beats a mock user', async () => {
75
217
  const { browser, addCookies } = createBrowser();
76
218
 
77
219
  await openPage({
@@ -81,11 +223,9 @@ test('openPage injects a per-call user with roles', async () => {
81
223
  user: { id: 'agent', roles: ['user-admin'] },
82
224
  });
83
225
 
84
- expect(decodeUserCookie(addCookies)).toEqual({
85
- sub: 'lowdefy-headless',
86
- id: 'agent',
87
- name: 'Lowdefy Headless',
88
- roles: ['user-admin'],
226
+ expect(decodeCallerCookie(addCookies)).toEqual({
227
+ user: { id: 'agent', name: 'Lowdefy Headless', roles: ['user-admin'] },
228
+ explicit: true,
89
229
  });
90
230
  });
91
231
 
@@ -120,3 +260,254 @@ test('openPage rejects an invalid user before opening a browser context', async
120
260
  ).rejects.toThrow('Headless "user" must be an object. Received "admin".');
121
261
  expect(browser.newContext).not.toHaveBeenCalled();
122
262
  });
263
+
264
+ test('openPage loads the page and waits on isPageReady for the page the app shows', async () => {
265
+ const { browser } = createBrowser();
266
+
267
+ const opened = await openPage({ browser, origin: 'http://localhost:3001', pageId: 'home' });
268
+
269
+ // Not networkidle: the dev reload event stream keeps the network busy for the
270
+ // life of the page, so that wait only ever ran out its timeout.
271
+ expect(opened.page.goto).toHaveBeenCalledTimes(1);
272
+ expect(opened.page.goto).toHaveBeenCalledWith('http://localhost:3001/home', {
273
+ waitUntil: 'load',
274
+ timeout: 15000,
275
+ });
276
+ // The page shown, so a redirect to the sign-in page settles too.
277
+ expect(opened.page.waitForFunction).toHaveBeenCalledWith(isPageReady, undefined, {
278
+ timeout: 15000,
279
+ });
280
+ expect(opened.ready).toBe(true);
281
+ });
282
+
283
+ test.each([
284
+ ['every image has loaded', [{ complete: true }], true],
285
+ ['an image is still loading', [{ complete: true }, { complete: false, loading: 'eager' }], false],
286
+ // Below the fold a lazy image never loads, so waiting on it would always
287
+ // run out the timeout.
288
+ [
289
+ 'the only one loading is lazy',
290
+ [{ complete: true }, { complete: false, loading: 'lazy' }],
291
+ true,
292
+ ],
293
+ ])('openPage treats the page images as loaded when %s', async (_, images, loaded) => {
294
+ const { browser, page } = createBrowser();
295
+
296
+ await openPage({ browser, origin: 'http://localhost:3001', pageId: 'home' });
297
+
298
+ const imagesLoaded = page.waitForFunction.mock.calls[1][0];
299
+ global.document = { images };
300
+ try {
301
+ expect(imagesLoaded()).toBe(loaded);
302
+ } finally {
303
+ delete global.document;
304
+ }
305
+ });
306
+
307
+ test('openPage resolves with ready false when the readiness wait times out', async () => {
308
+ const { browser, page } = createBrowser();
309
+ page.waitForFunction.mockRejectedValueOnce(new Error('Timeout 15000ms exceeded.'));
310
+
311
+ const opened = await openPage({ browser, origin: 'http://localhost:3001', pageId: 'home' });
312
+
313
+ expect(opened.ready).toBe(false);
314
+ expect(opened.page).toBe(page);
315
+ });
316
+
317
+ test('openPage closes the context it created when the navigation fails', async () => {
318
+ const { browser, context, page } = createBrowser();
319
+ page.goto.mockRejectedValue(new Error('Timeout 15000ms exceeded.'));
320
+
321
+ await expect(
322
+ openPage({ browser, origin: 'http://localhost:3001', pageId: 'home' })
323
+ ).rejects.toThrow('Timeout 15000ms exceeded.');
324
+ expect(context.close).toHaveBeenCalledTimes(1);
325
+ });
326
+
327
+ test('openPage closes the context it created when opening a page fails', async () => {
328
+ const { browser, context } = createBrowser();
329
+ context.newPage.mockRejectedValue(new Error('Browser crashed.'));
330
+
331
+ await expect(
332
+ openPage({ browser, origin: 'http://localhost:3001', pageId: 'home' })
333
+ ).rejects.toThrow('Browser crashed.');
334
+ expect(context.close).toHaveBeenCalledTimes(1);
335
+ });
336
+
337
+ test('buildPageUrl returns the bare page route without a urlQuery', () => {
338
+ expect(buildPageUrl({ origin: 'http://localhost:3001', pageId: 'home' })).toEqual(
339
+ 'http://localhost:3001/home'
340
+ );
341
+ expect(buildPageUrl({ origin: 'http://localhost:3001', pageId: 'home', urlQuery: {} })).toEqual(
342
+ 'http://localhost:3001/home'
343
+ );
344
+ });
345
+
346
+ test('buildPageUrl appends urlQuery the way the engine serializes Link urlQuery', () => {
347
+ expect(
348
+ buildPageUrl({
349
+ origin: 'http://localhost:3001',
350
+ pageId: 'detail',
351
+ urlQuery: { id: 'abc 1', page: 2, filter: { open: true } },
352
+ })
353
+ ).toEqual('http://localhost:3001/detail?id=abc+1&page=2&filter=%7B%22open%22%3Atrue%7D');
354
+ });
355
+
356
+ test('buildPageUrl fills the page pattern with pathParams, encoding each value', () => {
357
+ expect(
358
+ buildPageUrl({
359
+ origin: 'http://localhost:3001',
360
+ pageId: 'ticket',
361
+ path: 'tickets/{space}/{ticket_id}',
362
+ pathParams: { space: 'a b/c', ticket_id: 1 },
363
+ urlQuery: { tab: 'notes' },
364
+ })
365
+ ).toEqual('http://localhost:3001/tickets/a%20b%2Fc/1?tab=notes');
366
+ });
367
+
368
+ test('buildPageUrl serves a multi-segment page id without a path as is', () => {
369
+ expect(buildPageUrl({ origin: 'http://localhost:3001', pageId: 'support/inbox' })).toEqual(
370
+ 'http://localhost:3001/support/inbox'
371
+ );
372
+ });
373
+
374
+ test('openPage opens the page instance its pathParams name', async () => {
375
+ const { browser, page } = createBrowser();
376
+
377
+ const opened = await openPage({
378
+ browser,
379
+ origin: 'http://localhost:3001',
380
+ pageId: 'ticket',
381
+ path: 'tickets/{space}/{ticket_id}',
382
+ pathParams: { space: 's', ticket_id: '1' },
383
+ });
384
+
385
+ expect(opened.url).toEqual('http://localhost:3001/tickets/s/1');
386
+ expect(page.goto.mock.calls[0][0]).toEqual('http://localhost:3001/tickets/s/1');
387
+ });
388
+
389
+ test('openPage opens the page at the urlQuery it was given', async () => {
390
+ const { browser, page } = createBrowser();
391
+
392
+ const opened = await openPage({
393
+ browser,
394
+ origin: 'http://localhost:3001',
395
+ pageId: 'detail',
396
+ urlQuery: { id: '1' },
397
+ });
398
+
399
+ expect(opened.url).toEqual('http://localhost:3001/detail?id=1');
400
+ expect(page.goto.mock.calls[0][0]).toEqual('http://localhost:3001/detail?id=1');
401
+ });
402
+
403
+ test('openPage sets the data cookie with the actor cookie attributes before the first navigation', async () => {
404
+ const { journeyActorToken } = await import('../server/auth/journeyActor.js');
405
+ const { browser, addCookies, page } = createBrowser();
406
+ addCookies.mockImplementation(async () => {
407
+ expect(page.goto).not.toHaveBeenCalled();
408
+ });
409
+
410
+ await openPage({
411
+ browser,
412
+ origin: 'http://localhost:3001',
413
+ pageId: 'tickets',
414
+ clientAddress: '203.0.113.7',
415
+ dataCookie: 'session1',
416
+ });
417
+
418
+ const cookies = addCookies.mock.calls.map(([[cookie]]) => cookie);
419
+ const actor = cookies.find((cookie) => cookie.name === 'lowdefy_journey_actor');
420
+ const data = cookies.find((cookie) => cookie.name === 'lowdefy_journey_data');
421
+ expect(data).toEqual({
422
+ name: 'lowdefy_journey_data',
423
+ value: `${journeyActorToken}.session1`,
424
+ url: 'http://localhost:3001',
425
+ httpOnly: actor.httpOnly,
426
+ sameSite: actor.sameSite,
427
+ });
428
+ });
429
+
430
+ test('openPage sets no data cookie without a dataCookie', async () => {
431
+ const { browser, addCookies } = createBrowser();
432
+ await openPage({ browser, origin: 'http://localhost:3001', pageId: 'home', user: 'none' });
433
+ const names = addCookies.mock.calls.map(([[cookie]]) => cookie.name);
434
+ expect(names).not.toContain('lowdefy_journey_data');
435
+ });
436
+
437
+ test('openPage aborts and records a data set journey request to the dev server port on another host', async () => {
438
+ const { browser, context, page } = createBrowser();
439
+ context.route.mockImplementation(async () => {
440
+ expect(page.goto).not.toHaveBeenCalled();
441
+ });
442
+ const opened = await openPage({
443
+ browser,
444
+ origin: 'http://localhost:3001',
445
+ pageId: 'tickets',
446
+ dataCookie: 'session1',
447
+ });
448
+ expect(context.route).toHaveBeenCalledTimes(1);
449
+ const [[matches, handle]] = context.route.mock.calls;
450
+ for (const url of [
451
+ 'http://127.0.0.1:3001/api/page/tickets',
452
+ 'http://[::1]:3001/tickets',
453
+ 'http://192.168.1.20:3001/tickets',
454
+ 'https://127.0.0.1:3001/tickets',
455
+ ]) {
456
+ expect([url, matches(new URL(url))]).toEqual([url, true]);
457
+ }
458
+ for (const url of [
459
+ 'http://localhost:3001/api/page/tickets',
460
+ 'http://127.0.0.1:9000/assets/app.js',
461
+ 'https://example.com/logo.png',
462
+ 'http://127.0.0.1/tickets',
463
+ ]) {
464
+ expect([url, matches(new URL(url))]).toEqual([url, false]);
465
+ }
466
+ const route = {
467
+ request: () => ({ url: () => 'http://127.0.0.1:3001/api/page/tickets' }),
468
+ abort: jest.fn(async () => {}),
469
+ };
470
+ await handle(route);
471
+ expect(route.abort).toHaveBeenCalledTimes(1);
472
+ expect(opened.leftOrigin).toEqual(['http://127.0.0.1:3001/api/page/tickets']);
473
+ });
474
+
475
+ test('openPage matches the default port of an origin that names none', async () => {
476
+ const { browser, context } = createBrowser();
477
+ await openPage({ browser, origin: 'http://localhost', pageId: 'home', dataCookie: 'session1' });
478
+ const [[matches]] = context.route.mock.calls;
479
+ expect(matches(new URL('http://127.0.0.1/home'))).toBe(true);
480
+ expect(matches(new URL('http://127.0.0.1:80/home'))).toBe(true);
481
+ expect(matches(new URL('http://127.0.0.1:3001/home'))).toBe(false);
482
+ });
483
+
484
+ test('openPage routes nothing and records no departures without a dataCookie', async () => {
485
+ const { browser, context } = createBrowser();
486
+ const opened = await openPage({ browser, origin: 'http://localhost:3001', pageId: 'home' });
487
+ expect(context.route).not.toHaveBeenCalled();
488
+ expect(opened.leftOrigin).toEqual([]);
489
+ });
490
+
491
+ test('a failed openPage closes its context, so the idle browser still closes', async () => {
492
+ jest.useFakeTimers({ doNotFake: ['performance'] });
493
+ try {
494
+ const { browser, page } = createBrowser();
495
+ browser.isConnected = () => true;
496
+ browser.close = jest.fn(async () => {});
497
+ mockLaunch.mockResolvedValue(browser);
498
+ page.goto.mockRejectedValue(new Error('net::ERR_CONNECTION_REFUSED'));
499
+
500
+ const launched = await getBrowser();
501
+ await expect(
502
+ openPage({ browser: launched, origin: 'http://localhost:3001', pageId: 'home' })
503
+ ).rejects.toThrow('net::ERR_CONNECTION_REFUSED');
504
+ jest.advanceTimersByTime(90_000);
505
+ for (let i = 0; i < 5; i += 1) {
506
+ await Promise.resolve();
507
+ }
508
+
509
+ expect(browser.close).toHaveBeenCalledTimes(1);
510
+ } finally {
511
+ jest.useRealTimers();
512
+ }
513
+ });
@@ -0,0 +1,61 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+
20
+ import readBuildArtifact from './readBuildArtifact.js';
21
+
22
+ // The time of the latest successful config build. The manager rewrites
23
+ // build/buildStatus.json after every attempt, and a failed attempt keeps
24
+ // serving the previous build, so only an "ok" status moves this. The Hono
25
+ // process remembers the last one it read, since a failure overwrites it.
26
+ let publishedAt = 0;
27
+
28
+ function readPublishedAt() {
29
+ try {
30
+ const build = readBuildArtifact({ name: 'buildStatus.json' });
31
+ if (build?.status === 'ok' && build.timestamp) {
32
+ publishedAt = Math.max(publishedAt, Date.parse(build.timestamp));
33
+ }
34
+ } catch {
35
+ // A half-written status file; the completed write is read next time.
36
+ }
37
+ return publishedAt;
38
+ }
39
+
40
+ function readInvalidatedAt() {
41
+ try {
42
+ return (
43
+ Number(fs.readFileSync(path.join(process.cwd(), 'build', 'invalidatePages'), 'utf8')) || 0
44
+ );
45
+ } catch {
46
+ // No page edit since the last config build.
47
+ return 0;
48
+ }
49
+ }
50
+
51
+ // Identifies the build the dev server is serving: the time of the latest
52
+ // successful config build or page invalidation (the timestamp the manager
53
+ // writes to build/invalidatePages), whichever is later. Browser and server
54
+ // errors are stamped with it, so build status can tell errors from before the
55
+ // latest edit apart from live ones. Null before a successful build is seen.
56
+ function getBuildId() {
57
+ const latest = Math.max(readPublishedAt(), readInvalidatedAt());
58
+ return latest > 0 ? new Date(latest).toISOString() : null;
59
+ }
60
+
61
+ export default getBuildId;
@@ -0,0 +1,84 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+
21
+ // getBuildId reads build/buildStatus.json and build/invalidatePages from
22
+ // process.cwd(): point it at a throwaway server directory.
23
+ const serverDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-build-id-'));
24
+ fs.mkdirSync(path.join(serverDirectory, 'build'));
25
+ process.chdir(serverDirectory);
26
+
27
+ const { default: getBuildId } = await import('./getBuildId.js');
28
+
29
+ const statusPath = path.join(serverDirectory, 'build', 'buildStatus.json');
30
+ const invalidatePath = path.join(serverDirectory, 'build', 'invalidatePages');
31
+
32
+ afterEach(() => {
33
+ fs.rmSync(statusPath, { force: true });
34
+ fs.rmSync(invalidatePath, { force: true });
35
+ });
36
+
37
+ test('getBuildId is null before the first build', () => {
38
+ expect(getBuildId()).toBeNull();
39
+ });
40
+
41
+ test('getBuildId is the config build time when no page was invalidated since', () => {
42
+ fs.writeFileSync(
43
+ statusPath,
44
+ JSON.stringify({ status: 'ok', timestamp: '2026-03-01T10:00:00.000Z' })
45
+ );
46
+ fs.writeFileSync(invalidatePath, String(Date.parse('2026-03-01T09:00:00.000Z')));
47
+
48
+ expect(getBuildId()).toBe('2026-03-01T10:00:00.000Z');
49
+ });
50
+
51
+ test('getBuildId moves on with a page invalidation after the config build', () => {
52
+ fs.writeFileSync(
53
+ statusPath,
54
+ JSON.stringify({ status: 'ok', timestamp: '2026-03-01T10:00:00.000Z' })
55
+ );
56
+ fs.writeFileSync(invalidatePath, String(Date.parse('2026-03-01T10:05:00.000Z')));
57
+
58
+ expect(getBuildId()).toBe('2026-03-01T10:05:00.000Z');
59
+ });
60
+
61
+ test('getBuildId does not move when a config build fails', () => {
62
+ fs.writeFileSync(
63
+ statusPath,
64
+ JSON.stringify({ status: 'ok', timestamp: '2026-03-01T10:00:00.000Z' })
65
+ );
66
+ expect(getBuildId()).toBe('2026-03-01T10:00:00.000Z');
67
+
68
+ fs.writeFileSync(
69
+ statusPath,
70
+ JSON.stringify({ status: 'error', timestamp: '2026-03-01T11:00:00.000Z', errors: [{}] })
71
+ );
72
+ expect(getBuildId()).toBe('2026-03-01T10:00:00.000Z');
73
+
74
+ fs.writeFileSync(
75
+ statusPath,
76
+ JSON.stringify({ status: 'ok', timestamp: '2026-03-01T12:00:00.000Z' })
77
+ );
78
+ expect(getBuildId()).toBe('2026-03-01T12:00:00.000Z');
79
+ });
80
+
81
+ test('getBuildId keeps the last successful build when the status file is half written', () => {
82
+ fs.writeFileSync(statusPath, '{"status":"ok","timest');
83
+ expect(getBuildId()).toBe('2026-03-01T12:00:00.000Z');
84
+ });