@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
@@ -22,25 +22,27 @@ import resolveSource from './resolveSource.js';
22
22
  // headless-only. Falls back to headless whenever the tab path isn't usable
23
23
  // (no tab connected, or `source: 'tab'` was requested but it errored) so
24
24
  // agents always get an answer.
25
- async function evalOperator({ origin, pageId, expression, source, user }) {
25
+ async function evalOperator({ origin, pageId, pathParams, expression, source, user, data }) {
26
26
  const { tryTab, error, invalidInput } = resolveSource({
27
27
  name: 'evalOperator',
28
28
  pageId,
29
+ pathParams,
29
30
  source,
30
31
  user,
32
+ data,
31
33
  });
32
34
  if (error) {
33
35
  return { error, invalidInput };
34
36
  }
35
37
 
36
38
  if (tryTab) {
37
- const result = await evalOperatorInTab({ pageId, expression });
39
+ const result = await evalOperatorInTab({ pageId, pathParams, expression });
38
40
  if (!result?.error) {
39
41
  return { ...result, source: 'tab' };
40
42
  }
41
43
  }
42
44
 
43
- const result = await evalOperatorHeadless({ origin, pageId, expression, user });
45
+ const result = await evalOperatorHeadless({ origin, pageId, pathParams, expression, user, data });
44
46
  return { ...result, source: 'headless' };
45
47
  }
46
48
 
@@ -17,15 +17,32 @@
17
17
  import { type } from '@lowdefy/helpers';
18
18
 
19
19
  import { getBrowser, openPage, buildPageUrl } from './getBrowser.js';
20
+ import noBrowserError from './noBrowserError.js';
21
+ import resolvePageInstance from './resolvePageInstance.js';
22
+ import resolveToolCaller from './resolveToolCaller.js';
23
+ import unsettledPageNote from './unsettledPageNote.js';
24
+ import withBrowserSlot from './withBrowserSlot.js';
25
+ import withDataSession from './withDataSession.js';
20
26
 
21
27
  // Evaluates an operator expression against the live client state of a
22
- // headless Chromium tab navigated to the page's own route, using the page's
28
+ // headless Chromium tab navigated to the page instance's own route (`pathParams`
29
+ // fill a patterned page's placeholders), read under the instance key, using the page's
23
30
  // own WebParser instance so results match runtime exactly. Mirrors
24
31
  // Inspector.jsx's evalExpression (the live-tab equivalent).
25
- async function evalOperatorHeadless({ origin, pageId, expression, user, timeout = 15000 }) {
32
+ async function evalOperatorHeadless({
33
+ origin,
34
+ pageId,
35
+ pathParams,
36
+ expression,
37
+ user,
38
+ data,
39
+ timeout = 15000,
40
+ }) {
26
41
  if (type.isNone(origin) || !type.isString(origin)) {
27
42
  return {
28
- error: `evalOperatorHeadless requires an "origin" string. Received ${JSON.stringify(origin)}.`,
43
+ error: `evalOperatorHeadless requires an "origin" string. Received ${JSON.stringify(
44
+ origin
45
+ )}.`,
29
46
  };
30
47
  }
31
48
  if (type.isNone(pageId) || !type.isString(pageId)) {
@@ -37,30 +54,77 @@ async function evalOperatorHeadless({ origin, pageId, expression, user, timeout
37
54
  return { error: 'evalOperatorHeadless requires an "expression".' };
38
55
  }
39
56
 
57
+ const instance = resolvePageInstance({ pageId, pathParams });
58
+ if (!type.isUndefined(instance.error)) {
59
+ return { error: instance.error, invalidInput: true };
60
+ }
61
+
62
+ const caller = await resolveToolCaller({ user, data });
63
+ if (!type.isUndefined(caller.error)) {
64
+ return caller;
65
+ }
66
+
67
+ return withBrowserSlot({
68
+ task: () =>
69
+ withDataSession({
70
+ dataSet: caller.dataSet,
71
+ task: ({ dataCookie }) =>
72
+ evalOperatorInBrowser({
73
+ origin,
74
+ pageId,
75
+ pathParams,
76
+ instance,
77
+ expression,
78
+ user: caller.user,
79
+ dataCookie,
80
+ timeout,
81
+ }),
82
+ }),
83
+ });
84
+ }
85
+
86
+ // The part of evalOperatorHeadless that runs in the browser, inside a browser slot.
87
+ async function evalOperatorInBrowser({
88
+ origin,
89
+ pageId,
90
+ pathParams,
91
+ instance,
92
+ expression,
93
+ user,
94
+ dataCookie,
95
+ timeout,
96
+ }) {
40
97
  let browser;
41
98
  try {
42
99
  browser = await getBrowser();
43
100
  } catch (error) {
44
- return {
45
- error: `No Chromium available. Run: npx playwright install chromium (${error.message})`,
46
- };
101
+ return { error: noBrowserError(error) };
47
102
  }
48
103
 
49
- const url = buildPageUrl({ origin, pageId });
104
+ const url = buildPageUrl({ origin, pageId, path: instance.path, pathParams });
50
105
 
51
106
  let context;
52
107
  try {
53
- const opened = await openPage({ browser, origin, pageId, user, timeout });
108
+ const opened = await openPage({
109
+ browser,
110
+ origin,
111
+ pageId,
112
+ path: instance.path,
113
+ pathParams,
114
+ user,
115
+ dataCookie,
116
+ timeout,
117
+ });
54
118
  context = opened.context;
55
119
  // Passed through as JSON so the page-side parser always receives a plain
56
120
  // value, matching how it arrives at Inspector.jsx's eval-request handler.
57
121
  const expressionJson = JSON.stringify(expression);
58
122
  const result = await opened.page.evaluate(
59
- ({ id, exprJson }) => {
123
+ ({ id, instanceKey, path, exprJson }) => {
60
124
  const lowdefy = window.lowdefy;
61
- const pageContext = lowdefy?.contexts?.[`page:${id}`];
125
+ const pageContext = lowdefy?.contexts?.[instanceKey];
62
126
  if (!pageContext) {
63
- return { error: `No live context for page "${id}".` };
127
+ return { error: `No live context for page "${id}" at "/${path}".` };
64
128
  }
65
129
  const input = JSON.parse(exprJson);
66
130
  const { output, errors } = pageContext._internal.parser.parse({
@@ -72,8 +136,16 @@ async function evalOperatorHeadless({ origin, pageId, expression, user, timeout
72
136
  errors: errors.map((error) => error.message),
73
137
  };
74
138
  },
75
- { id: pageId, exprJson: expressionJson }
139
+ {
140
+ id: pageId,
141
+ instanceKey: instance.instanceKey,
142
+ path: instance.path,
143
+ exprJson: expressionJson,
144
+ }
76
145
  );
146
+ if (!opened.ready) {
147
+ return { ...result, ready: false, note: unsettledPageNote({ timeout }) };
148
+ }
77
149
  return result;
78
150
  } catch (error) {
79
151
  return { error: `Failed to evaluate operator at "${url}": ${error.message}` };
@@ -25,7 +25,10 @@ import { jest } from '@jest/globals';
25
25
  // screenshotPage.test.mjs, which exercises the same getBrowser.js singleton.
26
26
  jest.unstable_mockModule('playwright-core', () => ({
27
27
  chromium: { launch: jest.fn().mockRejectedValue(new Error("Executable doesn't exist")) },
28
+ selectors: { register: jest.fn(async () => {}) },
28
29
  }));
30
+ // A missing shell would otherwise start a real download.
31
+ jest.unstable_mockModule('./installHeadlessShell.js', () => ({ default: () => null }));
29
32
 
30
33
  // lib/build/config.js reads build/config.json from process.cwd() at import
31
34
  // time — chdir into a fixture that has one before getBrowser.js (which
@@ -35,6 +38,7 @@ const originalCwd = process.cwd();
35
38
  const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-eval-operator-test-'));
36
39
  fs.mkdirSync(path.join(fixtureDir, 'build'), { recursive: true });
37
40
  fs.writeFileSync(path.join(fixtureDir, 'build', 'config.json'), JSON.stringify({ basePath: '' }));
41
+ fs.writeFileSync(path.join(fixtureDir, 'build', 'routes.json'), JSON.stringify([]));
38
42
  process.chdir(fixtureDir);
39
43
 
40
44
  const { default: evalOperatorHeadless } = await import('./evalOperatorHeadless.js');
@@ -68,5 +72,7 @@ test('evalOperatorHeadless returns an actionable error when no browser is availa
68
72
  pageId: 'home',
69
73
  expression: { _state: 'x' },
70
74
  });
71
- expect(result.error).toMatch(/No Chromium available. Run: npx playwright install chromium/);
75
+ expect(result.error).toMatch(
76
+ /No Chromium available. Run: npx playwright install chromium-headless-shell/
77
+ );
72
78
  });
@@ -26,12 +26,13 @@ import { requestFromTab } from './tabChannel.js';
26
26
  // (see tabChannel.js) — `{ value, errors }` on success, or an `{ error }`
27
27
  // object if no tab is connected, the request timed out, or Inspector.jsx
28
28
  // itself failed to evaluate the expression.
29
- async function evalOperatorInTab({ pageId, expression }) {
29
+ async function evalOperatorInTab({ pageId, pathParams, expression }) {
30
30
  if (type.isNone(expression)) {
31
31
  throw new Error('evalOperatorInTab requires an "expression".');
32
32
  }
33
33
  const response = await requestFromTab({
34
34
  pageId,
35
+ pathParams,
35
36
  event: 'eval-request',
36
37
  payload: { expression },
37
38
  });
@@ -15,10 +15,12 @@
15
15
  */
16
16
 
17
17
  import path from 'node:path';
18
- import { resolveConfigLocation } from '@lowdefy/errors';
18
+ import { ConfigError, resolveConfigLocation } from '@lowdefy/errors';
19
19
  import { type } from '@lowdefy/helpers';
20
20
 
21
21
  import buildPageIfNeeded from '../server/jitPageBuilder.js';
22
+ import readMergedMaps from '../server/readMergedMaps.js';
23
+ import getHazards from './getHazards.js';
22
24
  import readBuildArtifact from './readBuildArtifact.js';
23
25
 
24
26
  const MAX_MATCHES = 20;
@@ -45,16 +47,11 @@ function buildIdPattern(id) {
45
47
  // (`root.pages[N:pageId]...`) carry the page segment inline, so a containment
46
48
  // pattern covers those.
47
49
  function buildPageScope({ pageId, keyMap }) {
48
- let rootKeyId = findPageRootKeyId({ pageId, keyMap });
49
- // A page built during the skeleton build (e.g. the default 404 page) chains
50
- // to the global config root, which every skeleton entry shares — useless as
51
- // a page discriminator. Those pages' keys carry the page segment inline, so
52
- // the pattern branch identifies them instead.
53
- if (!type.isNone(rootKeyId) && rootKeyId === findGlobalRootId(keyMap)) {
54
- rootKeyId = null;
55
- }
50
+ // A page built during the skeleton build (e.g. the default 404 page) has
51
+ // no JIT root at all — its keys carry the page segment inline, so the
52
+ // pattern branch identifies them instead.
56
53
  return {
57
- rootKeyId,
54
+ rootKeyId: findPageRootKeyId({ pageId, keyMap }),
58
55
  pagePattern: new RegExp(`\\bpages\\[\\d+:${escapeRegExp(pageId)}(:[^\\]]*)?\\]`),
59
56
  };
60
57
  }
@@ -68,38 +65,32 @@ function findGlobalRootId(keyMap) {
68
65
  return null;
69
66
  }
70
67
 
68
+ // A built page artifact mixes keys from two trees: the skeleton build keys
69
+ // the page stub (its `auth`, for one), chaining to the global config root,
70
+ // while the JIT build keys the page content under its own root. Only the
71
+ // JIT root discriminates pages, so skip keys that chain to the global root.
71
72
  function findPageRootKeyId({ pageId, keyMap }) {
72
73
  const artifact = readBuildArtifact({ name: `pages/${pageId}.json`, deserialize: true });
73
- const keyId = findFirstKeyId(artifact);
74
- if (type.isNone(keyId)) {
75
- return null;
74
+ const globalRootId = findGlobalRootId(keyMap);
75
+ for (const keyId of collectKeyIds(artifact)) {
76
+ const rootKeyId = resolveChainRootId({ keyId, keyMap });
77
+ if (rootKeyId !== globalRootId) {
78
+ return rootKeyId;
79
+ }
76
80
  }
77
- return resolveChainRootId({ keyId, keyMap });
81
+ return null;
78
82
  }
79
83
 
80
- function findFirstKeyId(node) {
84
+ function collectKeyIds(node, keyIds = []) {
81
85
  if (type.isString(node?.['~k'])) {
82
- return node['~k'];
86
+ keyIds.push(node['~k']);
83
87
  }
84
88
  if (type.isArray(node)) {
85
- for (const item of node) {
86
- const keyId = findFirstKeyId(item);
87
- if (keyId) {
88
- return keyId;
89
- }
90
- }
91
- return null;
92
- }
93
- if (!type.isObject(node)) {
94
- return null;
89
+ node.forEach((item) => collectKeyIds(item, keyIds));
90
+ } else if (type.isObject(node)) {
91
+ Object.keys(node).forEach((key) => collectKeyIds(node[key], keyIds));
95
92
  }
96
- for (const key of Object.keys(node)) {
97
- const keyId = findFirstKeyId(node[key]);
98
- if (keyId) {
99
- return keyId;
100
- }
101
- }
102
- return null;
93
+ return keyIds;
103
94
  }
104
95
 
105
96
  // Walk ~k_parent to the top of the entry's subtree. addKeys assigns the tree
@@ -129,7 +120,46 @@ function belongsToPage({ keyId, entry, keyMap, pageScope }) {
129
120
  return resolveChainRootId({ keyId, keyMap }) === pageScope.rootKeyId;
130
121
  }
131
122
 
132
- function scanKeyMap({ id, keyMap, refMap, configDirectory, pageScope }) {
123
+ // A key path ends in the matched node's own segment, `blocks[2:my_button:Button]`
124
+ // (addKeys recArray writes `name[index:id:type]`), which is all the kind and
125
+ // type information hazards need.
126
+ const NODE_SEGMENT = /([A-Za-z_]+)\[\d+:([^\]:]+)(?::([^\]]*))?\]$/;
127
+ const SEGMENT_KINDS = {
128
+ blocks: 'blocks',
129
+ connections: 'connections',
130
+ pages: 'pages',
131
+ requests: 'requests',
132
+ };
133
+
134
+ // writeRequests strips connectionId off the page's requests array after
135
+ // writing the per-request artifact, so a request's connection is only
136
+ // readable there. Without a pageId the match's page is not known, so the
137
+ // registry's pages are tried in turn — a requestId is unique within a page.
138
+ function findRequestConnectionId({ requestId, requestPageIds }) {
139
+ for (const pageId of requestPageIds) {
140
+ const request = readBuildArtifact({ name: `pages/${pageId}/requests/${requestId}.json` });
141
+ if (!type.isNone(request)) {
142
+ return request.connectionId ?? null;
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+
148
+ function hazardsForMatch({ keyPath, requestPageIds }) {
149
+ const segment = NODE_SEGMENT.exec(keyPath);
150
+ if (type.isNone(segment)) {
151
+ return [];
152
+ }
153
+ const [, arrayName, id, nodeType] = segment;
154
+ const kind = SEGMENT_KINDS[arrayName] ?? null;
155
+ let connectionId = null;
156
+ if (kind === 'requests') {
157
+ connectionId = findRequestConnectionId({ requestId: id, requestPageIds });
158
+ }
159
+ return getHazards({ kind, type: nodeType ?? null, connectionId });
160
+ }
161
+
162
+ function scanKeyMap({ id, keyMap, refMap, configDirectory, pageScope, requestPageIds }) {
133
163
  const pattern = buildIdPattern(id);
134
164
  const matches = [];
135
165
  for (const [keyId, entry] of Object.entries(keyMap)) {
@@ -148,6 +178,7 @@ function scanKeyMap({ id, keyMap, refMap, configDirectory, pageScope }) {
148
178
  matches.push({
149
179
  keyPath: entry.key,
150
180
  location: resolveConfigLocation({ configKey: keyId, keyMap, refMap, configDirectory }),
181
+ hazards: hazardsForMatch({ keyPath: entry.key, requestPageIds }),
151
182
  });
152
183
  }
153
184
  return matches;
@@ -163,8 +194,8 @@ function deIndexId(id) {
163
194
  return id.replace(/\.\d+(?=\.|$)/g, () => '.$');
164
195
  }
165
196
 
166
- function scanKeyMapWithDeIndex({ id, keyMap, refMap, configDirectory, pageScope }) {
167
- const matches = scanKeyMap({ id, keyMap, refMap, configDirectory, pageScope });
197
+ function scanKeyMapWithDeIndex({ id, keyMap, refMap, configDirectory, pageScope, requestPageIds }) {
198
+ const matches = scanKeyMap({ id, keyMap, refMap, configDirectory, pageScope, requestPageIds });
168
199
  if (matches.length > 0) {
169
200
  return matches;
170
201
  }
@@ -172,17 +203,25 @@ function scanKeyMapWithDeIndex({ id, keyMap, refMap, configDirectory, pageScope
172
203
  if (deIndexedId === id) {
173
204
  return matches;
174
205
  }
175
- return scanKeyMap({ id: deIndexedId, keyMap, refMap, configDirectory, pageScope });
206
+ return scanKeyMap({
207
+ id: deIndexedId,
208
+ keyMap,
209
+ refMap,
210
+ configDirectory,
211
+ pageScope,
212
+ requestPageIds,
213
+ });
176
214
  }
177
215
 
178
216
  // Locates a block/request/connection/etc by id, or a page by pageId. Block
179
- // content only exists in keyMap once its page has been JIT-built (see
180
- // lib/server/jitPageBuilder.js), so pass `pageId` to force-build and scan a
181
- // specific page. Never throws on not-found — agents should be able to treat
217
+ // content is only in the key maps once its page has been JIT-built (see
218
+ // lib/server/jitPageBuilder.js), and the dev server keeps the jitMaps/ files of
219
+ // its current and previous build contexts only, so pass `pageId` to force-build
220
+ // and scan a specific page. Never throws on not-found — agents should be able to treat
182
221
  // "no matches" as a normal result and try another id or pageId.
183
222
  async function findConfig({ id, pageId }) {
184
223
  if (type.isNone(id) || !type.isString(id)) {
185
- throw new Error(
224
+ throw new ConfigError(
186
225
  `findConfig requires an "id" string. Received ${JSON.stringify(id)}. ` +
187
226
  'Use GET /lowdefy-docs/find/:id.'
188
227
  );
@@ -190,6 +229,7 @@ async function findConfig({ id, pageId }) {
190
229
 
191
230
  const pageRegistry = readBuildArtifact({ name: 'pageRegistry.json' }) ?? {};
192
231
  const configDirectory = process.env.LOWDEFY_DIRECTORY_CONFIG || process.cwd();
232
+ const buildDirectory = path.join(process.cwd(), 'build');
193
233
 
194
234
  // A page's root block renders with blockId === pageId, so open-in-editor
195
235
  // and feedback enrichment hit this branch for it — they read
@@ -202,6 +242,7 @@ async function findConfig({ id, pageId }) {
202
242
  matches.push({
203
243
  keyPath: `pages[${id}]`,
204
244
  location: { source: path.resolve(configDirectory, refPath) },
245
+ hazards: getHazards({ kind: 'pages' }),
205
246
  });
206
247
  }
207
248
  return { kind: 'page', pageId: id, file: refPath, matches };
@@ -217,7 +258,6 @@ async function findConfig({ id, pageId }) {
217
258
  };
218
259
  }
219
260
 
220
- const buildDirectory = path.join(process.cwd(), 'build');
221
261
  try {
222
262
  await buildPageIfNeeded({ pageId, buildDirectory, configDirectory });
223
263
  } catch (error) {
@@ -227,14 +267,14 @@ async function findConfig({ id, pageId }) {
227
267
  };
228
268
  }
229
269
 
230
- const keyMap = readBuildArtifact({ name: 'keyMap.json' }) ?? {};
231
- const refMap = readBuildArtifact({ name: 'refMap.json' }) ?? {};
270
+ const { keyMap, refMap } = await readMergedMaps({ buildDirectory });
232
271
  const matches = scanKeyMapWithDeIndex({
233
272
  id,
234
273
  keyMap,
235
274
  refMap,
236
275
  configDirectory,
237
276
  pageScope: buildPageScope({ pageId, keyMap }),
277
+ requestPageIds: [pageId],
238
278
  });
239
279
  return matches.length > 0
240
280
  ? { matches }
@@ -246,14 +286,20 @@ async function findConfig({ id, pageId }) {
246
286
  };
247
287
  }
248
288
 
249
- const keyMap = readBuildArtifact({ name: 'keyMap.json' }) ?? {};
250
- const refMap = readBuildArtifact({ name: 'refMap.json' }) ?? {};
251
- const matches = scanKeyMapWithDeIndex({ id, keyMap, refMap, configDirectory });
289
+ const { keyMap, refMap } = await readMergedMaps({ buildDirectory });
290
+ const matches = scanKeyMapWithDeIndex({
291
+ id,
292
+ keyMap,
293
+ refMap,
294
+ configDirectory,
295
+ requestPageIds: Object.keys(pageRegistry),
296
+ });
252
297
  return {
253
298
  matches,
254
299
  note:
255
- 'Scanned without a pageId — content on pages that have not been JIT-built yet is not ' +
256
- 'in keyMap and was not covered. Pass ?pageId= to force-build and scan a specific page.',
300
+ 'Scanned without a pageId — only pages built since the last two page edits are ' +
301
+ 'covered; content on other pages was not scanned. Pass ?pageId= to force-build and ' +
302
+ 'scan a specific page.',
257
303
  };
258
304
  }
259
305
 
@@ -0,0 +1,81 @@
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 fs from 'node:fs';
19
+ import path from 'node:path';
20
+
21
+ // Largest response returned inline, in serialized characters. The tool result
22
+ // is pretty-printed, which adds roughly half again, so this keeps a result
23
+ // inside the output budget of common MCP clients.
24
+ const MAX_INLINE_RESPONSE_CHARS = 40_000;
25
+
26
+ // Saved responses kept per app; the oldest beyond this are deleted on write.
27
+ const MAX_SAVED_RESPONSES = 20;
28
+
29
+ function pruneResponseFiles({ directory }) {
30
+ const files = fs
31
+ .readdirSync(directory)
32
+ .filter((fileName) => fileName.endsWith('.json'))
33
+ .map((fileName) => {
34
+ const filePath = path.join(directory, fileName);
35
+ return { filePath, mtime: fs.statSync(filePath).mtimeMs };
36
+ })
37
+ .sort((a, b) => b.mtime - a.mtime);
38
+ files.slice(MAX_SAVED_RESPONSES).forEach(({ filePath }) => fs.rmSync(filePath, { force: true }));
39
+ }
40
+
41
+ function writeResponseFile({ name, response }) {
42
+ const configDirectory = process.env.LOWDEFY_DIRECTORY_CONFIG || process.cwd();
43
+ const directory = path.join(configDirectory, '.lowdefy', 'responses');
44
+ const safeName = name.replace(/[^a-zA-Z0-9_.-]/g, '_');
45
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
46
+ const suffix = crypto.randomBytes(3).toString('hex');
47
+ const filePath = path.join(directory, `${safeName}-${stamp}-${suffix}.json`);
48
+ fs.mkdirSync(directory, { recursive: true });
49
+ fs.writeFileSync(filePath, JSON.stringify(response, null, 2));
50
+ pruneResponseFiles({ directory });
51
+ return filePath;
52
+ }
53
+
54
+ // Returns the result untouched when its response fits inline. A larger
55
+ // response, or any response when saveResponse is set, is written in full to
56
+ // .lowdefy/responses/ in the config directory, and the result carries the
57
+ // file's path and size in its place, so nothing is cut and an agent can read
58
+ // or query the whole value.
59
+ function fitResponse({ result, name, saveResponse = false }) {
60
+ // A request can resolve with no response; it is written as null.
61
+ const { response = null, ...rest } = result;
62
+ const json = JSON.stringify(response);
63
+ if (!saveResponse && json.length <= MAX_INLINE_RESPONSE_CHARS) {
64
+ return result;
65
+ }
66
+ const fitted = {
67
+ ...rest,
68
+ responseFile: writeResponseFile({ name, response }),
69
+ responseChars: json.length,
70
+ };
71
+ if (Array.isArray(response)) {
72
+ fitted.responseItems = response.length;
73
+ }
74
+ fitted.note = saveResponse
75
+ ? 'The full response was written to responseFile.'
76
+ : `The response is ${json.length} characters, over the ${MAX_INLINE_RESPONSE_CHARS} returned inline, so it was written in full to responseFile. Read or query that file instead.`;
77
+ return fitted;
78
+ }
79
+
80
+ export { MAX_INLINE_RESPONSE_CHARS, MAX_SAVED_RESPONSES };
81
+ export default fitResponse;
@@ -0,0 +1,128 @@
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
+ import fitResponse, { MAX_INLINE_RESPONSE_CHARS, MAX_SAVED_RESPONSES } from './fitResponse.js';
22
+
23
+ let configDirectory;
24
+
25
+ beforeEach(() => {
26
+ configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-fit-response-'));
27
+ process.env.LOWDEFY_DIRECTORY_CONFIG = configDirectory;
28
+ });
29
+
30
+ afterEach(() => {
31
+ delete process.env.LOWDEFY_DIRECTORY_CONFIG;
32
+ });
33
+
34
+ test('fitResponse returns the result untouched when the response fits', () => {
35
+ const result = { refused: false, response: [{ _id: 1 }], status: 'success' };
36
+
37
+ expect(fitResponse({ result, name: 'home.get_rows' })).toBe(result);
38
+ expect(fs.existsSync(path.join(configDirectory, '.lowdefy'))).toBe(false);
39
+ });
40
+
41
+ test('fitResponse keeps a null response untouched', () => {
42
+ const result = { response: null, status: 'success' };
43
+
44
+ expect(fitResponse({ result, name: 'home.get_rows' })).toBe(result);
45
+ });
46
+
47
+ test('fitResponse keeps a result with no response untouched', () => {
48
+ // A failed routine, or one that ends without :return, has no response.
49
+ const result = {
50
+ error: { '~e': { name: 'ServiceError', message: 'Something went wrong.' } },
51
+ response: undefined,
52
+ status: 'error',
53
+ success: false,
54
+ };
55
+
56
+ expect(fitResponse({ result, name: 'search' })).toBe(result);
57
+ expect(fs.existsSync(path.join(configDirectory, '.lowdefy'))).toBe(false);
58
+ });
59
+
60
+ test('fitResponse writes an oversized response to a file and returns its path in its place', () => {
61
+ const rows = Array.from({ length: 500 }, (_, index) => ({ _id: index, title: 'x'.repeat(100) }));
62
+ const result = { response: rows, status: 'success', error: null };
63
+
64
+ const fitted = fitResponse({ result, name: 'tasks/list.get_rows' });
65
+
66
+ expect(fitted.response).toBeUndefined();
67
+ expect(fitted.status).toBe('success');
68
+ expect(fitted.error).toBeNull();
69
+ expect(fitted.responseItems).toBe(500);
70
+ expect(fitted.responseChars).toBe(JSON.stringify(rows).length);
71
+ expect(fitted.responseChars).toBeGreaterThan(MAX_INLINE_RESPONSE_CHARS);
72
+ expect(fitted.note).toContain('written in full to responseFile');
73
+ expect(path.dirname(fitted.responseFile)).toBe(
74
+ path.join(configDirectory, '.lowdefy', 'responses')
75
+ );
76
+ expect(path.basename(fitted.responseFile).startsWith('tasks_list.get_rows-')).toBe(true);
77
+ expect(JSON.parse(fs.readFileSync(fitted.responseFile, 'utf8'))).toEqual(rows);
78
+ expect(result.response).toBe(rows);
79
+ });
80
+
81
+ test('fitResponse writes a small response to a file when saveResponse is set', () => {
82
+ const result = { response: { total: 3 }, status: 'success' };
83
+
84
+ const fitted = fitResponse({ result, name: 'report', saveResponse: true });
85
+
86
+ expect(fitted.response).toBeUndefined();
87
+ expect(fitted.responseItems).toBeUndefined();
88
+ expect(fitted.note).toBe('The full response was written to responseFile.');
89
+ expect(JSON.parse(fs.readFileSync(fitted.responseFile, 'utf8'))).toEqual({ total: 3 });
90
+ });
91
+
92
+ test('fitResponse writes a missing response as null when saveResponse is set', () => {
93
+ const result = { status: 'success' };
94
+
95
+ const fitted = fitResponse({ result, name: 'home.update_row', saveResponse: true });
96
+
97
+ expect(fitted.responseChars).toBe(4);
98
+ expect(JSON.parse(fs.readFileSync(fitted.responseFile, 'utf8'))).toBeNull();
99
+ });
100
+
101
+ test('fitResponse gives every saved response its own file', () => {
102
+ const files = Array.from({ length: 5 }, (_, index) =>
103
+ fitResponse({ result: { response: { index } }, name: 'same', saveResponse: true })
104
+ ).map(({ responseFile }) => responseFile);
105
+
106
+ expect(new Set(files).size).toBe(5);
107
+ files.forEach((file, index) => {
108
+ expect(JSON.parse(fs.readFileSync(file, 'utf8'))).toEqual({ index });
109
+ });
110
+ });
111
+
112
+ test('fitResponse keeps only the most recent saved responses', () => {
113
+ const directory = path.join(configDirectory, '.lowdefy', 'responses');
114
+ fs.mkdirSync(directory, { recursive: true });
115
+ const old = path.join(directory, 'old-response.json');
116
+ fs.writeFileSync(old, '{}');
117
+ const longAgo = new Date('2000-01-01T00:00:00.000Z');
118
+ fs.utimesSync(old, longAgo, longAgo);
119
+
120
+ const files = Array.from({ length: MAX_SAVED_RESPONSES }, (_, index) =>
121
+ fitResponse({ result: { response: { index } }, name: 'report', saveResponse: true })
122
+ ).map(({ responseFile }) => responseFile);
123
+
124
+ const kept = fs.readdirSync(directory).map((fileName) => path.join(directory, fileName));
125
+ expect(kept).toHaveLength(MAX_SAVED_RESPONSES);
126
+ expect(kept).not.toContain(old);
127
+ expect(kept.sort()).toEqual([...files].sort());
128
+ });