@rubytech/create-maxy-code 0.1.552 → 0.1.553

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 (247) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/routine-templates/dist/index.d.ts +15 -2
  3. package/payload/platform/lib/routine-templates/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/routine-templates/dist/index.js +25 -1
  5. package/payload/platform/lib/routine-templates/dist/index.js.map +1 -1
  6. package/payload/platform/lib/routine-templates/dist/roster.d.ts +76 -1
  7. package/payload/platform/lib/routine-templates/dist/roster.d.ts.map +1 -1
  8. package/payload/platform/lib/routine-templates/dist/roster.js +60 -3
  9. package/payload/platform/lib/routine-templates/dist/roster.js.map +1 -1
  10. package/payload/platform/lib/routine-templates/src/__tests__/roster.test.ts +93 -1
  11. package/payload/platform/lib/routine-templates/src/__tests__/seed.test.ts +74 -0
  12. package/payload/platform/lib/routine-templates/src/index.ts +25 -1
  13. package/payload/platform/lib/routine-templates/src/roster.ts +111 -2
  14. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
  15. package/payload/platform/plugins/scheduling/PLUGIN.md +50 -4
  16. package/payload/platform/plugins/scheduling/mcp/dist/index.js +51 -6
  17. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-freebusy.test.d.ts +2 -0
  19. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-freebusy.test.d.ts.map +1 -0
  20. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-freebusy.test.js +216 -0
  21. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-freebusy.test.js.map +1 -0
  22. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/fires-per-day.test.d.ts +2 -0
  23. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/fires-per-day.test.d.ts.map +1 -0
  24. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/fires-per-day.test.js +44 -0
  25. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/fires-per-day.test.js.map +1 -0
  26. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.d.ts +2 -0
  27. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.d.ts.map +1 -0
  28. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.js +92 -0
  29. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.js.map +1 -0
  30. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js +210 -2
  31. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js.map +1 -1
  32. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/update-summary.test.js +18 -1
  33. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/update-summary.test.js.map +1 -1
  34. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-freebusy.d.ts +86 -0
  35. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-freebusy.d.ts.map +1 -0
  36. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-freebusy.js +240 -0
  37. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-freebusy.js.map +1 -0
  38. package/payload/platform/plugins/scheduling/mcp/dist/lib/fires-per-day.d.ts +2 -0
  39. package/payload/platform/plugins/scheduling/mcp/dist/lib/fires-per-day.d.ts.map +1 -0
  40. package/payload/platform/plugins/scheduling/mcp/dist/lib/fires-per-day.js +47 -0
  41. package/payload/platform/plugins/scheduling/mcp/dist/lib/fires-per-day.js.map +1 -0
  42. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts +1 -1
  43. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts.map +1 -1
  44. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js +9 -1
  45. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js.map +1 -1
  46. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts +103 -0
  47. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts.map +1 -1
  48. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js +307 -0
  49. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js.map +1 -1
  50. package/payload/platform/plugins/scheduling/mcp/dist/lib/update-summary.d.ts.map +1 -1
  51. package/payload/platform/plugins/scheduling/mcp/dist/lib/update-summary.js +11 -1
  52. package/payload/platform/plugins/scheduling/mcp/dist/lib/update-summary.js.map +1 -1
  53. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +41 -1
  54. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  55. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-connector-busy-sync.test.js +84 -1
  56. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-connector-busy-sync.test.js.map +1 -1
  57. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.d.ts +2 -0
  58. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.d.ts.map +1 -0
  59. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.js +200 -0
  60. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.js.map +1 -0
  61. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js +21 -1
  62. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js.map +1 -1
  63. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-recurrence-record.test.d.ts +2 -0
  64. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-recurrence-record.test.d.ts.map +1 -0
  65. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-recurrence-record.test.js +208 -0
  66. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-recurrence-record.test.js.map +1 -0
  67. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-connector-busy-sync.d.ts +26 -2
  68. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-connector-busy-sync.d.ts.map +1 -1
  69. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-connector-busy-sync.js +51 -2
  70. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-connector-busy-sync.js.map +1 -1
  71. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +40 -0
  72. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  73. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +70 -5
  74. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  75. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
  76. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +67 -0
  77. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
  78. package/payload/platform/plugins/workflows/skills/workflow-manager/SKILL.md +4 -0
  79. package/payload/server/public/activity.html +5 -5
  80. package/payload/server/public/agents.html +4 -4
  81. package/payload/server/public/assets/{AdminLoginScreens-Bjcs9ta9.js → AdminLoginScreens-Ca2QhL18.js} +1 -1
  82. package/payload/server/public/assets/AdminLoginScreens-Ca2QhL18.js.br +0 -0
  83. package/payload/server/public/assets/AdminLoginScreens-Ca2QhL18.js.gz +0 -0
  84. package/payload/server/public/assets/{AdminShell-DVBAMUCy.js → AdminShell-Dc5du5xX.js} +1 -1
  85. package/payload/server/public/assets/AdminShell-Dc5du5xX.js.br +0 -0
  86. package/payload/server/public/assets/AdminShell-Dc5du5xX.js.gz +0 -0
  87. package/payload/server/public/assets/{activity-C8E8-EwQ.js → activity-TSDKmIYb.js} +1 -1
  88. package/payload/server/public/assets/activity-TSDKmIYb.js.br +0 -0
  89. package/payload/server/public/assets/activity-TSDKmIYb.js.gz +0 -0
  90. package/payload/server/public/assets/{admin-CeWAlpRS.js → admin-Ba729Eyg.js} +1 -1
  91. package/payload/server/public/assets/admin-Ba729Eyg.js.br +0 -0
  92. package/payload/server/public/assets/admin-Ba729Eyg.js.gz +0 -0
  93. package/payload/server/public/assets/{agents-DuN92iFb.js → agents-9hlQHOS5.js} +1 -1
  94. package/payload/server/public/assets/agents-9hlQHOS5.js.br +0 -0
  95. package/payload/server/public/assets/agents-9hlQHOS5.js.gz +0 -0
  96. package/payload/server/public/assets/{browser-BxebdKbh.js → browser-BrqoCsqh.js} +1 -1
  97. package/payload/server/public/assets/browser-BrqoCsqh.js.br +0 -0
  98. package/payload/server/public/assets/browser-BrqoCsqh.js.gz +0 -0
  99. package/payload/server/public/assets/{calendar-BtFZQ5Np.js → calendar-DOObQ_m8.js} +1 -1
  100. package/payload/server/public/assets/calendar-DOObQ_m8.js.br +0 -0
  101. package/payload/server/public/assets/calendar-DOObQ_m8.js.gz +0 -0
  102. package/payload/server/public/assets/chat-Xaul2lLF.js +1 -0
  103. package/payload/server/public/assets/chat-Xaul2lLF.js.br +3 -0
  104. package/payload/server/public/assets/chat-Xaul2lLF.js.gz +0 -0
  105. package/payload/server/public/assets/chevron-left-BYH5vH4S.js +1 -0
  106. package/payload/server/public/assets/chevron-left-BYH5vH4S.js.br +0 -0
  107. package/payload/server/public/assets/chevron-right-XxQPHfNH.js +1 -0
  108. package/payload/server/public/assets/chevron-right-XxQPHfNH.js.br +0 -0
  109. package/payload/server/public/assets/clock-BNp2JTKh.js +1 -0
  110. package/payload/server/public/assets/clock-BNp2JTKh.js.br +0 -0
  111. package/payload/server/public/assets/clock-BNp2JTKh.js.gz +0 -0
  112. package/payload/server/public/assets/{copy-DCDHXq3O.js → copy-BkbgmpJo.js} +1 -1
  113. package/payload/server/public/assets/copy-BkbgmpJo.js.br +0 -0
  114. package/payload/server/public/assets/copy-BkbgmpJo.js.gz +0 -0
  115. package/payload/server/public/assets/data-yuHj8g0C.js +1 -0
  116. package/payload/server/public/assets/data-yuHj8g0C.js.br +3 -0
  117. package/payload/server/public/assets/data-yuHj8g0C.js.gz +0 -0
  118. package/payload/server/public/assets/{file-text-zQgEQWCF.js → file-text-DDCrqbWB.js} +1 -1
  119. package/payload/server/public/assets/file-text-DDCrqbWB.js.br +0 -0
  120. package/payload/server/public/assets/file-text-DDCrqbWB.js.gz +0 -0
  121. package/payload/server/public/assets/{graph-CFCEh_YI.js → graph-BKPdL5ph.js} +1 -1
  122. package/payload/server/public/assets/graph-BKPdL5ph.js.br +0 -0
  123. package/payload/server/public/assets/graph-BKPdL5ph.js.gz +0 -0
  124. package/payload/server/public/assets/{graph-labels-CA1UcDuS.js → graph-labels-B2PrHK6O.js} +1 -1
  125. package/payload/server/public/assets/graph-labels-B2PrHK6O.js.br +0 -0
  126. package/payload/server/public/assets/graph-labels-B2PrHK6O.js.gz +0 -0
  127. package/payload/server/public/assets/{operator-hFXPDc2X.js → operator-BVULmN09.js} +1 -1
  128. package/payload/server/public/assets/operator-BVULmN09.js.br +0 -0
  129. package/payload/server/public/assets/operator-BVULmN09.js.gz +0 -0
  130. package/payload/server/public/assets/{page-ne7I_r9L.js → page-BbzdSxuU.js} +1 -1
  131. package/payload/server/public/assets/page-BbzdSxuU.js.br +0 -0
  132. package/payload/server/public/assets/page-BbzdSxuU.js.gz +0 -0
  133. package/payload/server/public/assets/{page-C8jyPH_3.js → page-baQ9FQIg.js} +1 -1
  134. package/payload/server/public/assets/page-baQ9FQIg.js.br +0 -0
  135. package/payload/server/public/assets/page-baQ9FQIg.js.gz +0 -0
  136. package/payload/server/public/assets/play-3hlqzxLI.js +1 -0
  137. package/payload/server/public/assets/play-3hlqzxLI.js.br +0 -0
  138. package/payload/server/public/assets/play-3hlqzxLI.js.gz +0 -0
  139. package/payload/server/public/assets/{public-BgnqjAal.js → public-B_SEBxWy.js} +1 -1
  140. package/payload/server/public/assets/public-B_SEBxWy.js.br +0 -0
  141. package/payload/server/public/assets/public-B_SEBxWy.js.gz +0 -0
  142. package/payload/server/public/assets/{rotate-ccw-DfJWjXAg.js → rotate-ccw-DzXTXh3g.js} +1 -1
  143. package/payload/server/public/assets/rotate-ccw-DzXTXh3g.js.br +0 -0
  144. package/payload/server/public/assets/rotate-ccw-DzXTXh3g.js.gz +0 -0
  145. package/payload/server/public/assets/routines-BQzE0xzC.js +2 -0
  146. package/payload/server/public/assets/routines-BQzE0xzC.js.br +0 -0
  147. package/payload/server/public/assets/routines-BQzE0xzC.js.gz +0 -0
  148. package/payload/server/public/assets/{skills-D-6EbgfP.js → skills-yCZhYVIs.js} +1 -1
  149. package/payload/server/public/assets/skills-yCZhYVIs.js.br +0 -0
  150. package/payload/server/public/assets/skills-yCZhYVIs.js.gz +0 -0
  151. package/payload/server/public/assets/{tasks-DnN5Fyds.js → tasks-DxNFLsHv.js} +1 -1
  152. package/payload/server/public/assets/tasks-DxNFLsHv.js.br +0 -0
  153. package/payload/server/public/assets/tasks-DxNFLsHv.js.gz +0 -0
  154. package/payload/server/public/assets/{triangle-alert-eh9UOKDT.js → triangle-alert-CYwHTcvU.js} +1 -1
  155. package/payload/server/public/assets/triangle-alert-CYwHTcvU.js.br +0 -0
  156. package/payload/server/public/assets/triangle-alert-CYwHTcvU.js.gz +0 -0
  157. package/payload/server/public/assets/{useCopyFeedback-Bz9OJHBb.js → useCopyFeedback-BCRjKc-H.js} +1 -1
  158. package/payload/server/public/assets/useCopyFeedback-BCRjKc-H.js.br +0 -0
  159. package/payload/server/public/assets/useCopyFeedback-BCRjKc-H.js.gz +0 -0
  160. package/payload/server/public/assets/{useMediaQuery-CNxsjKZp.css → useMediaQuery-Vxpamxqq.css} +1 -1
  161. package/payload/server/public/assets/useMediaQuery-Vxpamxqq.css.br +0 -0
  162. package/payload/server/public/assets/{useMediaQuery-CNxsjKZp.css.gz → useMediaQuery-Vxpamxqq.css.gz} +0 -0
  163. package/payload/server/public/assets/{useVoiceRecorder-BaDpMYxX.js → useVoiceRecorder-BaAtMu9T.js} +1 -1
  164. package/payload/server/public/assets/useVoiceRecorder-BaAtMu9T.js.br +0 -0
  165. package/payload/server/public/assets/useVoiceRecorder-BaAtMu9T.js.gz +0 -0
  166. package/payload/server/public/assets/{wrench-BO-l46rF.js → wrench-iI-vx-g2.js} +1 -1
  167. package/payload/server/public/assets/wrench-iI-vx-g2.js.br +0 -0
  168. package/payload/server/public/assets/wrench-iI-vx-g2.js.gz +0 -0
  169. package/payload/server/public/browser.html +4 -4
  170. package/payload/server/public/calendar.html +8 -8
  171. package/payload/server/public/chat.html +13 -13
  172. package/payload/server/public/data.html +11 -11
  173. package/payload/server/public/graph.html +9 -9
  174. package/payload/server/public/index.html +14 -14
  175. package/payload/server/public/operator.html +14 -14
  176. package/payload/server/public/public.html +13 -13
  177. package/payload/server/public/routines.html +6 -6
  178. package/payload/server/public/skills.html +5 -5
  179. package/payload/server/public/tasks.html +6 -6
  180. package/payload/server/server.js +36 -5
  181. package/payload/server/public/assets/AdminLoginScreens-Bjcs9ta9.js.br +0 -0
  182. package/payload/server/public/assets/AdminLoginScreens-Bjcs9ta9.js.gz +0 -0
  183. package/payload/server/public/assets/AdminShell-DVBAMUCy.js.br +0 -0
  184. package/payload/server/public/assets/AdminShell-DVBAMUCy.js.gz +0 -0
  185. package/payload/server/public/assets/activity-C8E8-EwQ.js.br +0 -0
  186. package/payload/server/public/assets/activity-C8E8-EwQ.js.gz +0 -0
  187. package/payload/server/public/assets/admin-CeWAlpRS.js.br +0 -0
  188. package/payload/server/public/assets/admin-CeWAlpRS.js.gz +0 -0
  189. package/payload/server/public/assets/agents-DuN92iFb.js.br +0 -0
  190. package/payload/server/public/assets/agents-DuN92iFb.js.gz +0 -0
  191. package/payload/server/public/assets/browser-BxebdKbh.js.br +0 -0
  192. package/payload/server/public/assets/browser-BxebdKbh.js.gz +0 -0
  193. package/payload/server/public/assets/calendar-BtFZQ5Np.js.br +0 -0
  194. package/payload/server/public/assets/calendar-BtFZQ5Np.js.gz +0 -0
  195. package/payload/server/public/assets/chat-CGIwynaH.js +0 -1
  196. package/payload/server/public/assets/chat-CGIwynaH.js.br +0 -3
  197. package/payload/server/public/assets/chat-CGIwynaH.js.gz +0 -0
  198. package/payload/server/public/assets/chevron-left-DaNP1zDb.js +0 -1
  199. package/payload/server/public/assets/chevron-left-DaNP1zDb.js.br +0 -0
  200. package/payload/server/public/assets/chevron-right-BbbuTUnt.js +0 -1
  201. package/payload/server/public/assets/chevron-right-BbbuTUnt.js.br +0 -0
  202. package/payload/server/public/assets/clock-CfDdatjp.js +0 -1
  203. package/payload/server/public/assets/clock-CfDdatjp.js.br +0 -0
  204. package/payload/server/public/assets/clock-CfDdatjp.js.gz +0 -0
  205. package/payload/server/public/assets/copy-DCDHXq3O.js.br +0 -0
  206. package/payload/server/public/assets/copy-DCDHXq3O.js.gz +0 -0
  207. package/payload/server/public/assets/data-BchSWOju.js +0 -1
  208. package/payload/server/public/assets/data-BchSWOju.js.br +0 -1
  209. package/payload/server/public/assets/data-BchSWOju.js.gz +0 -0
  210. package/payload/server/public/assets/file-text-zQgEQWCF.js.br +0 -0
  211. package/payload/server/public/assets/file-text-zQgEQWCF.js.gz +0 -0
  212. package/payload/server/public/assets/graph-CFCEh_YI.js.br +0 -0
  213. package/payload/server/public/assets/graph-CFCEh_YI.js.gz +0 -0
  214. package/payload/server/public/assets/graph-labels-CA1UcDuS.js.br +0 -0
  215. package/payload/server/public/assets/graph-labels-CA1UcDuS.js.gz +0 -0
  216. package/payload/server/public/assets/operator-hFXPDc2X.js.br +0 -0
  217. package/payload/server/public/assets/operator-hFXPDc2X.js.gz +0 -0
  218. package/payload/server/public/assets/page-C8jyPH_3.js.br +0 -0
  219. package/payload/server/public/assets/page-C8jyPH_3.js.gz +0 -0
  220. package/payload/server/public/assets/page-ne7I_r9L.js.br +0 -0
  221. package/payload/server/public/assets/page-ne7I_r9L.js.gz +0 -0
  222. package/payload/server/public/assets/play-Ep0SZTbr.js +0 -1
  223. package/payload/server/public/assets/play-Ep0SZTbr.js.br +0 -0
  224. package/payload/server/public/assets/play-Ep0SZTbr.js.gz +0 -0
  225. package/payload/server/public/assets/public-BgnqjAal.js.br +0 -0
  226. package/payload/server/public/assets/public-BgnqjAal.js.gz +0 -0
  227. package/payload/server/public/assets/rotate-ccw-DfJWjXAg.js.br +0 -0
  228. package/payload/server/public/assets/rotate-ccw-DfJWjXAg.js.gz +0 -0
  229. package/payload/server/public/assets/routines-OGFXwTFO.js +0 -2
  230. package/payload/server/public/assets/routines-OGFXwTFO.js.br +0 -0
  231. package/payload/server/public/assets/routines-OGFXwTFO.js.gz +0 -0
  232. package/payload/server/public/assets/skills-D-6EbgfP.js.br +0 -0
  233. package/payload/server/public/assets/skills-D-6EbgfP.js.gz +0 -0
  234. package/payload/server/public/assets/tasks-DnN5Fyds.js.br +0 -0
  235. package/payload/server/public/assets/tasks-DnN5Fyds.js.gz +0 -0
  236. package/payload/server/public/assets/triangle-alert-eh9UOKDT.js.br +0 -0
  237. package/payload/server/public/assets/triangle-alert-eh9UOKDT.js.gz +0 -0
  238. package/payload/server/public/assets/useCopyFeedback-Bz9OJHBb.js.br +0 -0
  239. package/payload/server/public/assets/useCopyFeedback-Bz9OJHBb.js.gz +0 -0
  240. package/payload/server/public/assets/useMediaQuery-CNxsjKZp.css.br +0 -0
  241. package/payload/server/public/assets/useVoiceRecorder-BaDpMYxX.js.br +0 -0
  242. package/payload/server/public/assets/useVoiceRecorder-BaDpMYxX.js.gz +0 -0
  243. package/payload/server/public/assets/wrench-BO-l46rF.js.br +0 -0
  244. package/payload/server/public/assets/wrench-BO-l46rF.js.gz +0 -0
  245. /package/payload/server/public/assets/{useMediaQuery-_mMln3Fu.js → useMediaQuery-Baf4Huvv.js} +0 -0
  246. /package/payload/server/public/assets/{useMediaQuery-_mMln3Fu.js.br → useMediaQuery-Baf4Huvv.js.br} +0 -0
  247. /package/payload/server/public/assets/{useMediaQuery-_mMln3Fu.js.gz → useMediaQuery-Baf4Huvv.js.gz} +0 -0
@@ -0,0 +1,216 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { normaliseGraphInstant, providerEnv, readConnectorBusy, resolveConnector, } from "../connector-freebusy.js";
6
+ const WINDOW = { from: "2026-08-02T00:00:00.000Z", to: "2026-08-17T00:00:00.000Z" };
7
+ function accountWith(connectors) {
8
+ const dir = mkdtempSync(join(tmpdir(), "conn-"));
9
+ for (const c of connectors) {
10
+ const rel = c === "google" ? ["google", "accounts"] : ["outlook", "mailboxes"];
11
+ const idDir = join(dir, "secrets", ...rel, "id-1");
12
+ mkdirSync(idDir, { recursive: true });
13
+ writeFileSync(join(idDir, "tokens.enc"), "x");
14
+ }
15
+ return dir;
16
+ }
17
+ describe("resolveConnector", () => {
18
+ it("returns none when no connector is authorised", () => {
19
+ expect(resolveConnector(accountWith([]))).toEqual({ connector: "none", skipped: null });
20
+ });
21
+ it("returns the sole authorised connector", () => {
22
+ expect(resolveConnector(accountWith(["m365"]))).toEqual({ connector: "m365", skipped: null });
23
+ });
24
+ it("prefers google and names the skipped connector when both are authorised", () => {
25
+ expect(resolveConnector(accountWith(["google", "m365"]))).toEqual({
26
+ connector: "google",
27
+ skipped: "m365",
28
+ });
29
+ });
30
+ });
31
+ function platformWith(brand) {
32
+ const root = mkdtempSync(join(tmpdir(), "plat-"));
33
+ mkdirSync(join(root, "config"), { recursive: true });
34
+ writeFileSync(join(root, "config", "brand.json"), JSON.stringify(brand));
35
+ return root;
36
+ }
37
+ describe("providerEnv", () => {
38
+ it("carries the google client id and secret from brand.json", () => {
39
+ const root = platformWith({ googleClientId: "cid", googleClientSecret: "sec" });
40
+ const env = providerEnv(root, "acct-1", "google");
41
+ expect(env.GOOGLE_CLIENT_ID).toBe("cid");
42
+ expect(env.GOOGLE_CLIENT_SECRET).toBe("sec");
43
+ expect(env.ACCOUNT_ID).toBe("acct-1");
44
+ expect(env.PLATFORM_ROOT).toBe(root);
45
+ });
46
+ it("carries the outlook client id and defaults the tenant to common", () => {
47
+ const root = platformWith({ outlookClientId: "oid" });
48
+ const env = providerEnv(root, "acct-1", "m365");
49
+ expect(env.OUTLOOK_CLIENT_ID).toBe("oid");
50
+ expect(env.OUTLOOK_TENANT_ID).toBe("common");
51
+ });
52
+ it("carries an explicit outlook tenant when the brand ships one", () => {
53
+ const root = platformWith({ outlookClientId: "oid", outlookTenantId: "tid" });
54
+ expect(providerEnv(root, "acct-1", "m365").OUTLOOK_TENANT_ID).toBe("tid");
55
+ });
56
+ it("throws when the brand ships no google client id or secret", () => {
57
+ const root = platformWith({ googleClientId: "cid", googleClientSecret: "" });
58
+ expect(() => providerEnv(root, "acct-1", "google")).toThrow(/no Google vendor app/i);
59
+ });
60
+ it("throws when the brand ships no outlook client id", () => {
61
+ const root = platformWith({ outlookClientId: "" });
62
+ expect(() => providerEnv(root, "acct-1", "m365")).toThrow(/no Outlook vendor app/i);
63
+ });
64
+ it("throws when brand.json is absent rather than spawning with no credentials", () => {
65
+ const root = mkdtempSync(join(tmpdir(), "plat-"));
66
+ expect(() => providerEnv(root, "acct-1", "google")).toThrow(/brand\.json/);
67
+ });
68
+ });
69
+ describe("readConnectorBusy — google", () => {
70
+ it("flattens every calendar's busy list", async () => {
71
+ const calls = [];
72
+ const busy = await readConnectorBusy({
73
+ connector: "google",
74
+ platformRoot: "/p",
75
+ accountId: "acct-1",
76
+ ...WINDOW,
77
+ call: async (plugin, tool, args) => {
78
+ calls.push({ plugin, tool, args });
79
+ return {
80
+ primary: [{ start: "2026-08-03T09:00:00Z", end: "2026-08-03T10:00:00Z" }],
81
+ other: [{ start: "2026-08-04T09:00:00Z", end: "2026-08-04T10:00:00Z" }],
82
+ };
83
+ },
84
+ });
85
+ expect(calls).toHaveLength(1);
86
+ expect(calls[0].plugin).toBe("google");
87
+ expect(calls[0].tool).toBe("google-calendar-freebusy");
88
+ expect(calls[0].args).toEqual({ start: WINDOW.from, end: WINDOW.to });
89
+ expect(busy).toEqual([
90
+ { start: "2026-08-03T09:00:00Z", end: "2026-08-03T10:00:00Z" },
91
+ { start: "2026-08-04T09:00:00Z", end: "2026-08-04T10:00:00Z" },
92
+ ]);
93
+ });
94
+ it("returns an empty list for a genuinely free calendar", async () => {
95
+ const busy = await readConnectorBusy({
96
+ connector: "google",
97
+ platformRoot: "/p",
98
+ accountId: "acct-1",
99
+ ...WINDOW,
100
+ call: async () => ({ primary: [] }),
101
+ });
102
+ expect(busy).toEqual([]);
103
+ });
104
+ it("propagates a provider failure instead of returning an empty list", async () => {
105
+ await expect(readConnectorBusy({
106
+ connector: "google",
107
+ platformRoot: "/p",
108
+ accountId: "acct-1",
109
+ ...WINDOW,
110
+ call: async () => {
111
+ throw new Error("token refresh failed (400)");
112
+ },
113
+ })).rejects.toThrow(/token refresh failed/);
114
+ });
115
+ it("rejects a payload that is not the documented shape", async () => {
116
+ await expect(readConnectorBusy({
117
+ connector: "google",
118
+ platformRoot: "/p",
119
+ accountId: "acct-1",
120
+ ...WINDOW,
121
+ call: async () => "not an object",
122
+ })).rejects.toThrow(/unexpected google-calendar-freebusy payload/);
123
+ });
124
+ });
125
+ describe("normaliseGraphInstant", () => {
126
+ it("appends Z to a zone-less Graph dateTime", () => {
127
+ expect(normaliseGraphInstant("2026-08-03T09:00:00.0000000")).toBe("2026-08-03T09:00:00.0000000Z");
128
+ });
129
+ it("leaves an instant that already carries Z alone", () => {
130
+ expect(normaliseGraphInstant("2026-08-03T09:00:00Z")).toBe("2026-08-03T09:00:00Z");
131
+ });
132
+ it("leaves an instant that already carries an offset alone", () => {
133
+ expect(normaliseGraphInstant("2026-08-03T09:00:00+01:00")).toBe("2026-08-03T09:00:00+01:00");
134
+ });
135
+ });
136
+ describe("readConnectorBusy — m365", () => {
137
+ const okMailboxes = { mailboxes: [{ mailboxKey: "k1", mail: "ops@example.com", status: "ok" }] };
138
+ it("resolves the sole addressable mailbox, then reads its schedule in UTC", async () => {
139
+ const calls = [];
140
+ const busy = await readConnectorBusy({
141
+ connector: "m365",
142
+ platformRoot: "/p",
143
+ accountId: "acct-1",
144
+ ...WINDOW,
145
+ call: async (_plugin, tool, args) => {
146
+ calls.push({ tool, args });
147
+ if (tool === "outlook-mailbox-list")
148
+ return okMailboxes;
149
+ return {
150
+ requested: 1,
151
+ resolved: 1,
152
+ availability: [
153
+ {
154
+ address: "ops@example.com",
155
+ availabilityView: "0",
156
+ busy: [
157
+ { start: "2026-08-03T09:00:00.0000000", end: "2026-08-03T10:00:00.0000000", status: "busy" },
158
+ ],
159
+ error: null,
160
+ },
161
+ ],
162
+ };
163
+ },
164
+ });
165
+ expect(calls.map((c) => c.tool)).toEqual(["outlook-mailbox-list", "outlook-calendar-freebusy"]);
166
+ expect(calls[1].args).toEqual({
167
+ schedules: ["ops@example.com"],
168
+ start: { dateTime: WINDOW.from, timeZone: "UTC" },
169
+ end: { dateTime: WINDOW.to, timeZone: "UTC" },
170
+ });
171
+ expect(busy).toEqual([
172
+ { start: "2026-08-03T09:00:00.0000000Z", end: "2026-08-03T10:00:00.0000000Z" },
173
+ ]);
174
+ });
175
+ it("throws when no mailbox is addressable", async () => {
176
+ await expect(readConnectorBusy({
177
+ connector: "m365",
178
+ platformRoot: "/p",
179
+ accountId: "acct-1",
180
+ ...WINDOW,
181
+ call: async () => ({ mailboxes: [{ mailboxKey: "k1", mail: null, status: "blank-email" }] }),
182
+ })).rejects.toThrow(/no addressable Outlook mailbox/);
183
+ });
184
+ it("throws when two mailboxes are addressable rather than guessing one", async () => {
185
+ await expect(readConnectorBusy({
186
+ connector: "m365",
187
+ platformRoot: "/p",
188
+ accountId: "acct-1",
189
+ ...WINDOW,
190
+ call: async () => ({
191
+ mailboxes: [
192
+ { mailboxKey: "k1", mail: "a@example.com", status: "ok" },
193
+ { mailboxKey: "k2", mail: "b@example.com", status: "ok" },
194
+ ],
195
+ }),
196
+ })).rejects.toThrow(/two or more Outlook mailboxes/);
197
+ });
198
+ it("throws when Graph could not resolve the address instead of reading it as free", async () => {
199
+ await expect(readConnectorBusy({
200
+ connector: "m365",
201
+ platformRoot: "/p",
202
+ accountId: "acct-1",
203
+ ...WINDOW,
204
+ call: async (_p, tool) => tool === "outlook-mailbox-list"
205
+ ? okMailboxes
206
+ : {
207
+ requested: 1,
208
+ resolved: 0,
209
+ availability: [
210
+ { address: "ops@example.com", availabilityView: "", busy: [], error: "ErrorInvalidUser" },
211
+ ],
212
+ },
213
+ })).rejects.toThrow(/ErrorInvalidUser/);
214
+ });
215
+ });
216
+ //# sourceMappingURL=connector-freebusy.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connector-freebusy.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/connector-freebusy.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=fires-per-day.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fires-per-day.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/fires-per-day.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,44 @@
1
+ // Task 2380. The count is per calendar day IN THE EVENT'S OWN ZONE, not per
2
+ // rolling 24 hours, which is the whole reason the clock-change cases below read
3
+ // 23 and 25 rather than 24. A rolling window would report 24 on every day of the
4
+ // year and the number would drift twice a year without anything saying so.
5
+ import { describe, it, expect } from "vitest";
6
+ import { firesPerDay } from "../fires-per-day.js";
7
+ const LONDON = "Europe/London";
8
+ const at = (iso) => new Date(iso);
9
+ describe("firesPerDay (Task 2380)", () => {
10
+ it("counts the incident's own change: hourly is 24, quarter-hourly is 96", () => {
11
+ const ref = at("2026-08-02T12:00:00Z");
12
+ expect(firesPerDay("0 * * * *", LONDON, ref)).toBe(24);
13
+ expect(firesPerDay("*/15 * * * *", LONDON, ref)).toBe(96);
14
+ });
15
+ it("reads 25 on the day British clocks go back", () => {
16
+ const ref = at("2026-10-25T12:00:00Z");
17
+ expect(firesPerDay("0 * * * *", LONDON, ref)).toBe(25);
18
+ expect(firesPerDay("*/15 * * * *", LONDON, ref)).toBe(100);
19
+ });
20
+ it("reads 23 on the day British clocks go forward", () => {
21
+ const ref = at("2026-03-29T12:00:00Z");
22
+ expect(firesPerDay("0 * * * *", LONDON, ref)).toBe(23);
23
+ expect(firesPerDay("*/15 * * * *", LONDON, ref)).toBe(92);
24
+ });
25
+ it("counts the day the reference falls in when the reference IS local midnight", () => {
26
+ // 23:00Z is 00:00 on 3 August in London. The boundary search must land on
27
+ // that midnight, not step back to the previous day's.
28
+ expect(firesPerDay("0 * * * *", LONDON, at("2026-08-02T23:00:00Z"))).toBe(24);
29
+ });
30
+ it("counts a daily cron once", () => {
31
+ expect(firesPerDay("0 9 * * *", LONDON, at("2026-08-02T12:00:00Z"))).toBe(1);
32
+ });
33
+ it("counts a weekday-only cron as zero on a Sunday", () => {
34
+ // 2026-08-02 is a Sunday.
35
+ expect(firesPerDay("0 9 * * 1-5", LONDON, at("2026-08-02T12:00:00Z"))).toBe(0);
36
+ });
37
+ it("falls back to the server zone when the row stores none", () => {
38
+ expect(firesPerDay("0 * * * *", null, at("2026-08-02T12:00:00Z"))).toBe(24);
39
+ });
40
+ it("throws on an expression that does not parse, so callers can report it", () => {
41
+ expect(() => firesPerDay("not a cron", LONDON, at("2026-08-02T12:00:00Z"))).toThrow();
42
+ });
43
+ });
44
+ //# sourceMappingURL=fires-per-day.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fires-per-day.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/fires-per-day.test.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,gFAAgF;AAChF,iFAAiF;AACjF,2EAA2E;AAC3E,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,MAAM,MAAM,GAAG,eAAe,CAAC;AAC/B,MAAM,EAAE,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC;AAE1C,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,GAAG,GAAG,EAAE,CAAC,sBAAsB,CAAC,CAAC;QACvC,MAAM,CAAC,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvD,MAAM,CAAC,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,GAAG,GAAG,EAAE,CAAC,sBAAsB,CAAC,CAAC;QACvC,MAAM,CAAC,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvD,MAAM,CAAC,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,GAAG,GAAG,EAAE,CAAC,sBAAsB,CAAC,CAAC;QACvC,MAAM,CAAC,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvD,MAAM,CAAC,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4EAA4E,EAAE,GAAG,EAAE;QACpF,0EAA0E;QAC1E,sDAAsD;QACtD,MAAM,CAAC,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;QAClC,MAAM,CAAC,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC/E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,0BAA0B;QAC1B,MAAM,CAAC,WAAW,CAAC,aAAa,EAAE,MAAM,EAAE,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,CAAC,WAAW,CAAC,WAAW,EAAE,IAAI,EAAE,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IACxF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=gate-classification.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-classification.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/gate-classification.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,92 @@
1
+ // @vitest-environment node
2
+ // Task 2383 — how the fleet's recurring agent routines answered the gate
3
+ // question.
4
+ //
5
+ // The two Task 2378 checks watch a gate that is configured. This one watches
6
+ // whether one was configured at all, because the drift it measures is a
7
+ // population moving rather than an action failing: every waiver is individually
8
+ // reasonable and the fleet still ends up ungated.
9
+ //
10
+ // The zero case is asserted as hard as the alarm. A check that goes quiet when
11
+ // healthy cannot be distinguished from one that has stopped running.
12
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
13
+ import { auditGateClassification } from "../schedule-audit.js";
14
+ const ACCT = "20028fb5-0000-4000-8000-000000000000";
15
+ const OTHER = "9f3c1d2e-0000-4000-8000-000000000000";
16
+ /** A driver returning one fixed record set, or throwing on the read. */
17
+ function driverOf(rows, throws = false) {
18
+ return {
19
+ session: () => ({
20
+ run: async () => {
21
+ if (throws)
22
+ throw new Error("neo4j down");
23
+ return { records: rows.map((r) => ({ get: (k) => r[k] ?? null })) };
24
+ },
25
+ close: async () => { },
26
+ }),
27
+ };
28
+ }
29
+ let lines;
30
+ beforeEach(() => {
31
+ lines = [];
32
+ vi.spyOn(console, "error").mockImplementation((...args) => {
33
+ lines.push(args.map(String).join(" "));
34
+ });
35
+ });
36
+ afterEach(() => {
37
+ vi.restoreAllMocks();
38
+ });
39
+ function census(acct = ACCT) {
40
+ return lines.find((l) => l.includes(`op=gate-classification acct=${acct.slice(0, 8)}`));
41
+ }
42
+ describe("auditGateClassification", () => {
43
+ it("classifies each row exactly once", async () => {
44
+ await auditGateClassification(driverOf([
45
+ { accountId: ACCT, gatePlugin: "scheduling", gateWaived: null },
46
+ { accountId: ACCT, gatePlugin: null, gateWaived: "daily briefing" },
47
+ { accountId: ACCT, gatePlugin: null, gateWaived: null },
48
+ ]), [ACCT]);
49
+ expect(census()).toMatch(/agentRoutines=3 gated=1 waived=1 unclassified=1 ok=false/);
50
+ });
51
+ it("counts a row holding both a gate and a waiver as gated", async () => {
52
+ // The gate is what the dispatcher acts on, so it decides the bucket.
53
+ await auditGateClassification(driverOf([{ accountId: ACCT, gatePlugin: "scheduling", gateWaived: "belt and braces" }]), [ACCT]);
54
+ expect(census()).toMatch(/agentRoutines=1 gated=1 waived=0 unclassified=0 ok=true/);
55
+ });
56
+ it("treats a blank waiver as unclassified", async () => {
57
+ // Same rule as the write: blank is absent, so a row that slipped past with
58
+ // an empty string is not reported as a declared exception.
59
+ await auditGateClassification(driverOf([{ accountId: ACCT, gatePlugin: null, gateWaived: " " }]), [ACCT]);
60
+ expect(census()).toMatch(/waived=0 unclassified=1 ok=false/);
61
+ });
62
+ it("never fails on the waived count alone", async () => {
63
+ // Waived carries no threshold. The drift it measures is a judgement the
64
+ // operator makes across weeks, not a condition code can decide.
65
+ await auditGateClassification(driverOf([
66
+ { accountId: ACCT, gatePlugin: null, gateWaived: "weekly digest" },
67
+ { accountId: ACCT, gatePlugin: null, gateWaived: "daily briefing" },
68
+ { accountId: ACCT, gatePlugin: null, gateWaived: "monthly recap" },
69
+ ]), [ACCT]);
70
+ expect(census()).toMatch(/waived=3 unclassified=0 ok=true/);
71
+ });
72
+ it("emits a zero line for an account with no agent routines", async () => {
73
+ await auditGateClassification(driverOf([]), [ACCT]);
74
+ expect(census()).toMatch(/agentRoutines=0 gated=0 waived=0 unclassified=0 ok=true/);
75
+ });
76
+ it("emits one line per local account", async () => {
77
+ await auditGateClassification(driverOf([{ accountId: ACCT, gatePlugin: "scheduling", gateWaived: null }]), [ACCT, OTHER]);
78
+ expect(census(ACCT)).toMatch(/agentRoutines=1 gated=1/);
79
+ expect(census(OTHER)).toMatch(/agentRoutines=0 gated=0 waived=0 unclassified=0 ok=true/);
80
+ });
81
+ it("emits the unavailable line INSTEAD of a census when the read fails", async () => {
82
+ // An unreadable census must never be mistaken for a fully classified fleet.
83
+ await auditGateClassification(driverOf([], true), [ACCT]);
84
+ expect(census()).toBeUndefined();
85
+ expect(lines.some((l) => l.includes("op=gate-classification-unavailable reason=neo4j down"))).toBe(true);
86
+ });
87
+ it("does nothing with no local accounts", async () => {
88
+ await auditGateClassification(driverOf([]), []);
89
+ expect(lines).toHaveLength(0);
90
+ });
91
+ });
92
+ //# sourceMappingURL=gate-classification.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-classification.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/gate-classification.test.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  // Task 2100. The audit exists because the failure it guards emitted nothing:
2
2
  // the write logged ok, the read logged ok, and the two disagreed about which
3
3
  // label the calendar lives on.
4
- import { describe, it, expect, vi } from "vitest";
5
- import { auditScheduleVisibility } from "../schedule-audit.js";
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
5
+ import { auditScheduleVisibility, auditFireRateCensus } from "../schedule-audit.js";
6
6
  /** Counts :Meeting and :Event for the account; the list function is injected. */
7
7
  function countingSession(meetings, events) {
8
8
  return {
@@ -45,4 +45,212 @@ describe("auditScheduleVisibility (Task 2100)", () => {
45
45
  expect(list).toHaveBeenCalledWith({ accountId: "acct-1", limit: 11 });
46
46
  });
47
47
  });
48
+ // Task 2381. The connector that silently resolves to `none`: a revoked
49
+ // authorisation makes every fire sync an empty busy list, the action succeeds,
50
+ // the RoutineRun closes clean, and the booking page offers slots the operator is
51
+ // not free for.
52
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
53
+ import { tmpdir } from "node:os";
54
+ import { join } from "node:path";
55
+ import { auditConnectorResolution, nextConnectorState, noneForHours, } from "../schedule-audit.js";
56
+ const T0 = Date.parse("2026-08-02T00:00:00.000Z");
57
+ const HOUR = 3_600_000;
58
+ describe("nextConnectorState", () => {
59
+ it("starts the none clock on the first none observation", () => {
60
+ expect(nextConnectorState(undefined, "none", T0)).toEqual({ connector: "none", noneSinceMs: T0 });
61
+ });
62
+ it("keeps the original none instant across consecutive none observations", () => {
63
+ const first = nextConnectorState(undefined, "none", T0);
64
+ expect(nextConnectorState(first, "none", T0 + HOUR).noneSinceMs).toBe(T0);
65
+ });
66
+ it("clears the none clock when a connector comes back", () => {
67
+ const first = nextConnectorState(undefined, "none", T0);
68
+ expect(nextConnectorState(first, "google", T0 + HOUR)).toEqual({
69
+ connector: "google",
70
+ noneSinceMs: null,
71
+ });
72
+ });
73
+ it("restarts the clock after a recovery, so the run length is never cumulative", () => {
74
+ const none1 = nextConnectorState(undefined, "none", T0);
75
+ const back = nextConnectorState(none1, "google", T0 + HOUR);
76
+ expect(nextConnectorState(back, "none", T0 + 2 * HOUR).noneSinceMs).toBe(T0 + 2 * HOUR);
77
+ });
78
+ });
79
+ describe("noneForHours", () => {
80
+ it("is 0 while a connector is resolved", () => {
81
+ expect(noneForHours({ connector: "google", noneSinceMs: null }, T0)).toBe(0);
82
+ });
83
+ it("counts whole hours since the none run began", () => {
84
+ expect(noneForHours({ connector: "none", noneSinceMs: T0 }, T0 + 25 * HOUR)).toBe(25);
85
+ });
86
+ });
87
+ /** An accounts directory whose named accounts carry the given connectors. */
88
+ function accountsDir(spec) {
89
+ const dir = mkdtempSync(join(tmpdir(), "audit-accts-"));
90
+ for (const [accountId, connectors] of Object.entries(spec)) {
91
+ mkdirSync(join(dir, accountId), { recursive: true });
92
+ for (const c of connectors) {
93
+ const rel = c === "google" ? ["google", "accounts"] : ["outlook", "mailboxes"];
94
+ const idDir = join(dir, accountId, "secrets", ...rel, "id-1");
95
+ mkdirSync(idDir, { recursive: true });
96
+ writeFileSync(join(idDir, "tokens.enc"), "x");
97
+ }
98
+ }
99
+ return dir;
100
+ }
101
+ /** A driver whose one query returns the accounts holding a scheduled busy-sync
102
+ * routine. */
103
+ function driverWithSyncRoutines(accountIds) {
104
+ const session = {
105
+ run: vi.fn(async () => ({
106
+ records: accountIds.map((id) => ({ get: () => id })),
107
+ })),
108
+ close: vi.fn(async () => { }),
109
+ };
110
+ return { driver: { session: () => session }, session };
111
+ }
112
+ describe("auditConnectorResolution", () => {
113
+ it("emits one line per local account per tick, including a healthy one", async () => {
114
+ const logs = [];
115
+ const spy = vi.spyOn(console, "error").mockImplementation((m) => void logs.push(String(m)));
116
+ const { driver } = driverWithSyncRoutines(["acct-1", "acct-2"]);
117
+ const statePath = join(mkdtempSync(join(tmpdir(), "audit-state-")), "s.json");
118
+ await auditConnectorResolution(driver, ["acct-1", "acct-2"], accountsDir({ "acct-1": ["google"], "acct-2": [] }), statePath, T0);
119
+ spy.mockRestore();
120
+ const lines = logs.filter((l) => l.includes("op=connector-resolution"));
121
+ expect(lines).toHaveLength(2);
122
+ expect(lines[0]).toContain("connector=google");
123
+ expect(lines[0]).toContain("noneForHours=0");
124
+ expect(lines[0]).toContain("ok=true");
125
+ // A fresh `none` is not yet an alarm — it is one tick old, not 24 hours.
126
+ expect(lines[1]).toContain("connector=none");
127
+ expect(lines[1]).toContain("ok=true");
128
+ });
129
+ it("reports ok=false once an account has resolved none for over 24 hours", async () => {
130
+ const dir = accountsDir({ "acct-1": [] });
131
+ const statePath = join(mkdtempSync(join(tmpdir(), "audit-state-")), "s.json");
132
+ const { driver } = driverWithSyncRoutines(["acct-1"]);
133
+ await auditConnectorResolution(driver, ["acct-1"], dir, statePath, T0);
134
+ const logs = [];
135
+ const spy = vi.spyOn(console, "error").mockImplementation((m) => void logs.push(String(m)));
136
+ await auditConnectorResolution(driver, ["acct-1"], dir, statePath, T0 + 25 * HOUR);
137
+ spy.mockRestore();
138
+ const line = logs.find((l) => l.includes("op=connector-resolution"));
139
+ expect(line).toContain("noneForHours=25");
140
+ expect(line).toContain("ok=false");
141
+ });
142
+ it("does not alarm on an account with no scheduled busy-sync routine", async () => {
143
+ const dir = accountsDir({ "acct-1": [] });
144
+ const statePath = join(mkdtempSync(join(tmpdir(), "audit-state-")), "s.json");
145
+ const { driver } = driverWithSyncRoutines([]);
146
+ await auditConnectorResolution(driver, ["acct-1"], dir, statePath, T0);
147
+ const logs = [];
148
+ const spy = vi.spyOn(console, "error").mockImplementation((m) => void logs.push(String(m)));
149
+ await auditConnectorResolution(driver, ["acct-1"], dir, statePath, T0 + 25 * HOUR);
150
+ spy.mockRestore();
151
+ const line = logs.find((l) => l.includes("op=connector-resolution"));
152
+ expect(line).toContain("noneForHours=25");
153
+ expect(line).toContain("ok=true");
154
+ });
155
+ it("persists state so the run length survives the next heartbeat process", async () => {
156
+ const dir = accountsDir({ "acct-1": [] });
157
+ const statePath = join(mkdtempSync(join(tmpdir(), "audit-state-")), "s.json");
158
+ const { driver } = driverWithSyncRoutines(["acct-1"]);
159
+ await auditConnectorResolution(driver, ["acct-1"], dir, statePath, T0);
160
+ expect(existsSync(statePath)).toBe(true);
161
+ expect(JSON.parse(readFileSync(statePath, "utf-8"))["acct-1"]).toEqual({
162
+ connector: "none",
163
+ noneSinceMs: T0,
164
+ });
165
+ });
166
+ it("never throws — a failed query costs one line, not the tick", async () => {
167
+ const session = {
168
+ run: vi.fn(async () => {
169
+ throw new Error("neo4j down");
170
+ }),
171
+ close: vi.fn(async () => { }),
172
+ };
173
+ const logs = [];
174
+ const spy = vi.spyOn(console, "error").mockImplementation((m) => void logs.push(String(m)));
175
+ await expect(auditConnectorResolution({ session: () => session }, ["acct-1"], accountsDir({ "acct-1": [] }), join(mkdtempSync(join(tmpdir(), "audit-state-")), "s.json"), T0)).resolves.toBeUndefined();
176
+ spy.mockRestore();
177
+ expect(logs.some((l) => l.includes("op=connector-resolution-error"))).toBe(true);
178
+ });
179
+ });
180
+ function censusDriver(rows) {
181
+ return {
182
+ session: () => ({
183
+ run: vi.fn(async () => ({
184
+ records: rows.map((r) => ({
185
+ get: (k) => r[k] ?? null,
186
+ })),
187
+ })),
188
+ close: vi.fn(async () => { }),
189
+ }),
190
+ };
191
+ }
192
+ const censusRow = (over = {}) => ({
193
+ accountId: "acct-1111-2222",
194
+ eventId: "evt-3333-4444",
195
+ recurrence: "0 * * * *",
196
+ timezone: "Europe/London",
197
+ eventStatus: "scheduled",
198
+ ...over,
199
+ });
200
+ describe("auditFireRateCensus (Task 2380)", () => {
201
+ let censusLogs = [];
202
+ beforeEach(() => {
203
+ censusLogs = [];
204
+ // Only Date is faked: the per-day counts are real assertions, and on a live
205
+ // clock the two British clock-change days would produce different numbers.
206
+ vi.useFakeTimers({ toFake: ["Date"] });
207
+ vi.setSystemTime(new Date("2026-08-02T12:00:00Z"));
208
+ vi.spyOn(console, "error").mockImplementation((...args) => {
209
+ censusLogs.push(args.map(String).join(" "));
210
+ });
211
+ });
212
+ afterEach(() => {
213
+ vi.useRealTimers();
214
+ vi.restoreAllMocks();
215
+ });
216
+ const censusLine = () => censusLogs.find((l) => l.includes("op=fire-rate-census "));
217
+ it("totals the daily fires of every agent-bearing scheduled routine", async () => {
218
+ await auditFireRateCensus(censusDriver([censusRow(), censusRow({ recurrence: "*/15 * * * *" })]), ["acct-1111-2222"]);
219
+ expect(censusLine()).toContain("agentRoutines=2");
220
+ expect(censusLine()).toContain("agentFiresPerDay=120");
221
+ expect(censusLine()).toContain("ok=true");
222
+ });
223
+ it("counts a suspended routine in neither figure, because its rate is zero", async () => {
224
+ await auditFireRateCensus(censusDriver([censusRow(), censusRow({ eventStatus: "suspended", recurrence: "*/15 * * * *" })]), ["acct-1111-2222"]);
225
+ expect(censusLine()).toContain("agentRoutines=1");
226
+ expect(censusLine()).toContain("agentFiresPerDay=24");
227
+ });
228
+ it("emits a line for an account with no rows at all", async () => {
229
+ // A check that goes quiet when healthy cannot be told apart from one that
230
+ // stopped running.
231
+ await auditFireRateCensus(censusDriver([]), ["acct-1111-2222"]);
232
+ expect(censusLine()).toContain("agentRoutines=0");
233
+ expect(censusLine()).toContain("agentFiresPerDay=0");
234
+ expect(censusLine()).toContain("ok=true");
235
+ });
236
+ it("fails the vacuity guard when agent rows exist but none is counted", async () => {
237
+ // Task 2231's convention: a zero meaning "the enumeration found nothing"
238
+ // must not read the same as a zero meaning "there is nothing".
239
+ await auditFireRateCensus(censusDriver([censusRow({ eventStatus: "suspended" })]), [
240
+ "acct-1111-2222",
241
+ ]);
242
+ expect(censusLine()).toContain("agentRoutines=0");
243
+ expect(censusLine()).toContain("ok=false");
244
+ });
245
+ it("names an unparsable row and keeps the rest of the total", async () => {
246
+ await auditFireRateCensus(censusDriver([censusRow(), censusRow({ eventId: "evt-bad", recurrence: "not a cron" })]), ["acct-1111-2222"]);
247
+ expect(censusLogs.find((l) => l.includes("op=fire-rate-census-unparsable"))).toContain("eventId=evt-bad");
248
+ expect(censusLine()).toContain("agentRoutines=1");
249
+ expect(censusLine()).toContain("agentFiresPerDay=24");
250
+ });
251
+ it("emits nothing when there are no local accounts", async () => {
252
+ await auditFireRateCensus(censusDriver([censusRow()]), []);
253
+ expect(censusLine()).toBeUndefined();
254
+ });
255
+ });
48
256
  //# sourceMappingURL=schedule-audit.test.js.map