@rubytech/create-maxy-code 0.1.590 → 0.1.592

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 (243) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/routine-templates/dist/index.d.ts +2 -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 +2 -9
  5. package/payload/platform/lib/routine-templates/dist/index.js.map +1 -1
  6. package/payload/platform/lib/routine-templates/dist/roster.d.ts +1 -11
  7. package/payload/platform/lib/routine-templates/dist/roster.d.ts.map +1 -1
  8. package/payload/platform/lib/routine-templates/dist/roster.js +11 -15
  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 +0 -0
  11. package/payload/platform/lib/routine-templates/src/__tests__/seed.test.ts +8 -3
  12. package/payload/platform/lib/routine-templates/src/index.ts +1 -8
  13. package/payload/platform/lib/routine-templates/src/roster.ts +10 -14
  14. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +13 -0
  15. package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
  16. package/payload/platform/plugins/outlook/PLUGIN.md +6 -1
  17. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-gate-probe.test.d.ts +2 -0
  18. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-gate-probe.test.d.ts.map +1 -0
  19. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-gate-probe.test.js +75 -0
  20. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-gate-probe.test.js.map +1 -0
  21. package/payload/platform/plugins/outlook/mcp/dist/index.js +14 -0
  22. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  23. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-gate-probe.d.ts +33 -0
  24. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-gate-probe.d.ts.map +1 -0
  25. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-gate-probe.js +32 -0
  26. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-gate-probe.js.map +1 -0
  27. package/payload/platform/plugins/scheduling/PLUGIN.md +11 -5
  28. package/payload/platform/plugins/scheduling/mcp/dist/index.js +4 -12
  29. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  30. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.js +85 -1
  31. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.js.map +1 -1
  32. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.js +55 -1
  33. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.js.map +1 -1
  34. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts +10 -1
  35. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts.map +1 -1
  36. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js +42 -6
  37. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js.map +1 -1
  38. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts +22 -0
  39. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts.map +1 -1
  40. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js +73 -0
  41. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js.map +1 -1
  42. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js +79 -0
  43. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js.map +1 -1
  44. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +2 -1
  45. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
  46. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +40 -1
  47. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
  48. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +19 -2
  49. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  50. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js +339 -275
  51. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js.map +1 -1
  52. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js +510 -96
  53. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js.map +1 -1
  54. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +62 -31
  55. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  56. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +153 -93
  57. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  58. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
  59. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +8 -36
  60. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
  61. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts +234 -30
  62. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts.map +1 -1
  63. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js +349 -38
  64. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -1
  65. package/payload/platform/scripts/check-specialist-tool-surface.mjs +15 -1
  66. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  67. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
  68. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  69. package/payload/server/public/activity.html +6 -6
  70. package/payload/server/public/agents.html +5 -5
  71. package/payload/server/public/assets/{AdminLoginScreens-CemYu7ho.js → AdminLoginScreens-dBZJlMbp.js} +1 -1
  72. package/payload/server/public/assets/AdminLoginScreens-dBZJlMbp.js.br +0 -0
  73. package/payload/server/public/assets/AdminLoginScreens-dBZJlMbp.js.gz +0 -0
  74. package/payload/server/public/assets/{AdminShell-B5vEeSLw.js → AdminShell-D94sgtsb.js} +1 -1
  75. package/payload/server/public/assets/AdminShell-D94sgtsb.js.br +0 -0
  76. package/payload/server/public/assets/AdminShell-D94sgtsb.js.gz +0 -0
  77. package/payload/server/public/assets/{activity-BHSuZrqG.js → activity-D6NEUvT7.js} +1 -1
  78. package/payload/server/public/assets/activity-D6NEUvT7.js.br +0 -0
  79. package/payload/server/public/assets/activity-D6NEUvT7.js.gz +0 -0
  80. package/payload/server/public/assets/{admin-DnvauP59.js → admin-BluqXSKn.js} +1 -1
  81. package/payload/server/public/assets/admin-BluqXSKn.js.br +0 -0
  82. package/payload/server/public/assets/admin-BluqXSKn.js.gz +0 -0
  83. package/payload/server/public/assets/{agents-CwTJjfyV.js → agents-D_6V66dr.js} +1 -1
  84. package/payload/server/public/assets/agents-D_6V66dr.js.br +0 -0
  85. package/payload/server/public/assets/agents-D_6V66dr.js.gz +0 -0
  86. package/payload/server/public/assets/{browser-D4yjnKEH.js → browser-Bz9s6ztS.js} +1 -1
  87. package/payload/server/public/assets/browser-Bz9s6ztS.js.br +0 -0
  88. package/payload/server/public/assets/browser-Bz9s6ztS.js.gz +0 -0
  89. package/payload/server/public/assets/{calendar-DUi1XFyY.js → calendar-D5OCrmwA.js} +1 -1
  90. package/payload/server/public/assets/calendar-D5OCrmwA.js.br +0 -0
  91. package/payload/server/public/assets/calendar-D5OCrmwA.js.gz +0 -0
  92. package/payload/server/public/assets/chat-CjlbEzZ-.js +1 -0
  93. package/payload/server/public/assets/chat-CjlbEzZ-.js.br +1 -0
  94. package/payload/server/public/assets/chat-CjlbEzZ-.js.gz +0 -0
  95. package/payload/server/public/assets/chevron-left-b_r2jNvg.js +1 -0
  96. package/payload/server/public/assets/chevron-left-b_r2jNvg.js.br +0 -0
  97. package/payload/server/public/assets/clock-D8tMbEZD.js +1 -0
  98. package/payload/server/public/assets/clock-D8tMbEZD.js.br +0 -0
  99. package/payload/server/public/assets/clock-D8tMbEZD.js.gz +0 -0
  100. package/payload/server/public/assets/{copy-Bw2h4ofb.js → copy-CXBu2lHL.js} +1 -1
  101. package/payload/server/public/assets/copy-CXBu2lHL.js.br +0 -0
  102. package/payload/server/public/assets/copy-CXBu2lHL.js.gz +0 -0
  103. package/payload/server/public/assets/data-ChHg_Lq9.js +1 -0
  104. package/payload/server/public/assets/data-ChHg_Lq9.js.br +0 -0
  105. package/payload/server/public/assets/data-ChHg_Lq9.js.gz +0 -0
  106. package/payload/server/public/assets/{field-BS-mu5Jx.js → field-D4fXu_SP.js} +1 -1
  107. package/payload/server/public/assets/field-D4fXu_SP.js.br +0 -0
  108. package/payload/server/public/assets/field-D4fXu_SP.js.gz +0 -0
  109. package/payload/server/public/assets/{file-text-xUlDn7ui.js → file-text-BxZ1UMcQ.js} +1 -1
  110. package/payload/server/public/assets/file-text-BxZ1UMcQ.js.br +0 -0
  111. package/payload/server/public/assets/file-text-BxZ1UMcQ.js.gz +0 -0
  112. package/payload/server/public/assets/{graph-DEEXiQLx.js → graph-DMleOCBZ.js} +1 -1
  113. package/payload/server/public/assets/graph-DMleOCBZ.js.br +0 -0
  114. package/payload/server/public/assets/graph-DMleOCBZ.js.gz +0 -0
  115. package/payload/server/public/assets/{graph-labels-F6hQJKCn.js → graph-labels-BHapdrTm.js} +1 -1
  116. package/payload/server/public/assets/graph-labels-BHapdrTm.js.br +0 -0
  117. package/payload/server/public/assets/graph-labels-BHapdrTm.js.gz +0 -0
  118. package/payload/server/public/assets/{jsx-runtime-DDrveXI8.css → jsx-runtime-gqFnq1MC.css} +1 -1
  119. package/payload/server/public/assets/jsx-runtime-gqFnq1MC.css.br +0 -0
  120. package/payload/server/public/assets/{jsx-runtime-DDrveXI8.css.gz → jsx-runtime-gqFnq1MC.css.gz} +0 -0
  121. package/payload/server/public/assets/{operator-CkWK9uRB.js → operator-BpZTRiga.js} +1 -1
  122. package/payload/server/public/assets/operator-BpZTRiga.js.br +0 -0
  123. package/payload/server/public/assets/operator-BpZTRiga.js.gz +0 -0
  124. package/payload/server/public/assets/{page-Dh6fCuUi.js → page-CsHWZZ0U.js} +1 -1
  125. package/payload/server/public/assets/page-CsHWZZ0U.js.br +0 -0
  126. package/payload/server/public/assets/page-CsHWZZ0U.js.gz +0 -0
  127. package/payload/server/public/assets/{page-DbWIOK60.js → page-Dczx6CPP.js} +1 -1
  128. package/payload/server/public/assets/page-Dczx6CPP.js.br +0 -0
  129. package/payload/server/public/assets/page-Dczx6CPP.js.gz +0 -0
  130. package/payload/server/public/assets/play-D_xdb6gy.js +1 -0
  131. package/payload/server/public/assets/play-D_xdb6gy.js.br +0 -0
  132. package/payload/server/public/assets/play-D_xdb6gy.js.gz +0 -0
  133. package/payload/server/public/assets/{public-DmD6vvFM.js → public-BzpELWaZ.js} +1 -1
  134. package/payload/server/public/assets/public-BzpELWaZ.js.br +0 -0
  135. package/payload/server/public/assets/public-BzpELWaZ.js.gz +0 -0
  136. package/payload/server/public/assets/{rotate-ccw-BDdOWTDl.js → rotate-ccw-Bkpst1na.js} +1 -1
  137. package/payload/server/public/assets/rotate-ccw-Bkpst1na.js.br +0 -0
  138. package/payload/server/public/assets/rotate-ccw-Bkpst1na.js.gz +0 -0
  139. package/payload/server/public/assets/{routines-CZoKUK0p.js → routines-Cmuf8pPP.js} +2 -2
  140. package/payload/server/public/assets/routines-Cmuf8pPP.js.br +0 -0
  141. package/payload/server/public/assets/routines-Cmuf8pPP.js.gz +0 -0
  142. package/payload/server/public/assets/{skills-D0yS0qLX.js → skills-Bfq3Pbw-.js} +1 -1
  143. package/payload/server/public/assets/skills-Bfq3Pbw-.js.br +0 -0
  144. package/payload/server/public/assets/skills-Bfq3Pbw-.js.gz +0 -0
  145. package/payload/server/public/assets/{tasks-Dtn1uDRY.js → tasks-nyWLrolq.js} +1 -1
  146. package/payload/server/public/assets/tasks-nyWLrolq.js.br +0 -0
  147. package/payload/server/public/assets/tasks-nyWLrolq.js.gz +0 -0
  148. package/payload/server/public/assets/{triangle-alert-DN-Eswuf.js → triangle-alert-DkENIQJ6.js} +1 -1
  149. package/payload/server/public/assets/triangle-alert-DkENIQJ6.js.br +0 -0
  150. package/payload/server/public/assets/triangle-alert-DkENIQJ6.js.gz +0 -0
  151. package/payload/server/public/assets/{useCopyFeedback-BmOZr_Mw.js → useCopyFeedback-DmNjbXe9.js} +1 -1
  152. package/payload/server/public/assets/useCopyFeedback-DmNjbXe9.js.br +1 -0
  153. package/payload/server/public/assets/useCopyFeedback-DmNjbXe9.js.gz +0 -0
  154. package/payload/server/public/assets/{useMediaQuery-fFM7rxUD.js → useMediaQuery-DL-C1y_F.js} +1 -1
  155. package/payload/server/public/assets/useMediaQuery-DL-C1y_F.js.br +0 -0
  156. package/payload/server/public/assets/useMediaQuery-DL-C1y_F.js.gz +0 -0
  157. package/payload/server/public/assets/{useVoiceRecorder-D0MWFIde.js → useVoiceRecorder-DseLVULD.js} +1 -1
  158. package/payload/server/public/assets/useVoiceRecorder-DseLVULD.js.br +0 -0
  159. package/payload/server/public/assets/useVoiceRecorder-DseLVULD.js.gz +0 -0
  160. package/payload/server/public/assets/{wrench-C-yn7q1Q.js → wrench-C7tdp6z9.js} +1 -1
  161. package/payload/server/public/assets/wrench-C7tdp6z9.js.br +0 -0
  162. package/payload/server/public/assets/wrench-C7tdp6z9.js.gz +0 -0
  163. package/payload/server/public/browser.html +5 -5
  164. package/payload/server/public/calendar.html +8 -8
  165. package/payload/server/public/chat.html +14 -14
  166. package/payload/server/public/data.html +12 -12
  167. package/payload/server/public/field.html +3 -3
  168. package/payload/server/public/graph.html +10 -10
  169. package/payload/server/public/index.html +14 -14
  170. package/payload/server/public/operator.html +15 -15
  171. package/payload/server/public/public.html +14 -14
  172. package/payload/server/public/routines.html +8 -8
  173. package/payload/server/public/skills.html +6 -6
  174. package/payload/server/public/tasks.html +7 -7
  175. package/payload/server/server.js +41 -2
  176. package/payload/server/public/assets/AdminLoginScreens-CemYu7ho.js.br +0 -0
  177. package/payload/server/public/assets/AdminLoginScreens-CemYu7ho.js.gz +0 -0
  178. package/payload/server/public/assets/AdminShell-B5vEeSLw.js.br +0 -0
  179. package/payload/server/public/assets/AdminShell-B5vEeSLw.js.gz +0 -0
  180. package/payload/server/public/assets/activity-BHSuZrqG.js.br +0 -0
  181. package/payload/server/public/assets/activity-BHSuZrqG.js.gz +0 -0
  182. package/payload/server/public/assets/admin-DnvauP59.js.br +0 -0
  183. package/payload/server/public/assets/admin-DnvauP59.js.gz +0 -0
  184. package/payload/server/public/assets/agents-CwTJjfyV.js.br +0 -0
  185. package/payload/server/public/assets/agents-CwTJjfyV.js.gz +0 -0
  186. package/payload/server/public/assets/browser-D4yjnKEH.js.br +0 -0
  187. package/payload/server/public/assets/browser-D4yjnKEH.js.gz +0 -0
  188. package/payload/server/public/assets/calendar-DUi1XFyY.js.br +0 -0
  189. package/payload/server/public/assets/calendar-DUi1XFyY.js.gz +0 -0
  190. package/payload/server/public/assets/chat-BJ1p4kuh.js +0 -1
  191. package/payload/server/public/assets/chat-BJ1p4kuh.js.br +0 -1
  192. package/payload/server/public/assets/chat-BJ1p4kuh.js.gz +0 -0
  193. package/payload/server/public/assets/chevron-left-Ctw-3JsV.js +0 -1
  194. package/payload/server/public/assets/chevron-left-Ctw-3JsV.js.br +0 -0
  195. package/payload/server/public/assets/clock-BLOp42yY.js +0 -1
  196. package/payload/server/public/assets/clock-BLOp42yY.js.br +0 -0
  197. package/payload/server/public/assets/clock-BLOp42yY.js.gz +0 -0
  198. package/payload/server/public/assets/copy-Bw2h4ofb.js.br +0 -0
  199. package/payload/server/public/assets/copy-Bw2h4ofb.js.gz +0 -0
  200. package/payload/server/public/assets/data-uR7BZKVU.js +0 -1
  201. package/payload/server/public/assets/data-uR7BZKVU.js.br +0 -0
  202. package/payload/server/public/assets/data-uR7BZKVU.js.gz +0 -0
  203. package/payload/server/public/assets/field-BS-mu5Jx.js.br +0 -0
  204. package/payload/server/public/assets/field-BS-mu5Jx.js.gz +0 -0
  205. package/payload/server/public/assets/file-text-xUlDn7ui.js.br +0 -0
  206. package/payload/server/public/assets/file-text-xUlDn7ui.js.gz +0 -0
  207. package/payload/server/public/assets/graph-DEEXiQLx.js.br +0 -0
  208. package/payload/server/public/assets/graph-DEEXiQLx.js.gz +0 -0
  209. package/payload/server/public/assets/graph-labels-F6hQJKCn.js.br +0 -0
  210. package/payload/server/public/assets/graph-labels-F6hQJKCn.js.gz +0 -0
  211. package/payload/server/public/assets/jsx-runtime-DDrveXI8.css.br +0 -0
  212. package/payload/server/public/assets/operator-CkWK9uRB.js.br +0 -0
  213. package/payload/server/public/assets/operator-CkWK9uRB.js.gz +0 -0
  214. package/payload/server/public/assets/page-DbWIOK60.js.br +0 -0
  215. package/payload/server/public/assets/page-DbWIOK60.js.gz +0 -0
  216. package/payload/server/public/assets/page-Dh6fCuUi.js.br +0 -0
  217. package/payload/server/public/assets/page-Dh6fCuUi.js.gz +0 -0
  218. package/payload/server/public/assets/play-fy3tdi7M.js +0 -1
  219. package/payload/server/public/assets/play-fy3tdi7M.js.br +0 -0
  220. package/payload/server/public/assets/play-fy3tdi7M.js.gz +0 -0
  221. package/payload/server/public/assets/public-DmD6vvFM.js.br +0 -0
  222. package/payload/server/public/assets/public-DmD6vvFM.js.gz +0 -0
  223. package/payload/server/public/assets/rotate-ccw-BDdOWTDl.js.br +0 -0
  224. package/payload/server/public/assets/rotate-ccw-BDdOWTDl.js.gz +0 -0
  225. package/payload/server/public/assets/routines-CZoKUK0p.js.br +0 -0
  226. package/payload/server/public/assets/routines-CZoKUK0p.js.gz +0 -0
  227. package/payload/server/public/assets/skills-D0yS0qLX.js.br +0 -0
  228. package/payload/server/public/assets/skills-D0yS0qLX.js.gz +0 -0
  229. package/payload/server/public/assets/tasks-Dtn1uDRY.js.br +0 -0
  230. package/payload/server/public/assets/tasks-Dtn1uDRY.js.gz +0 -0
  231. package/payload/server/public/assets/triangle-alert-DN-Eswuf.js.br +0 -0
  232. package/payload/server/public/assets/triangle-alert-DN-Eswuf.js.gz +0 -0
  233. package/payload/server/public/assets/useCopyFeedback-BmOZr_Mw.js.br +0 -0
  234. package/payload/server/public/assets/useCopyFeedback-BmOZr_Mw.js.gz +0 -0
  235. package/payload/server/public/assets/useMediaQuery-fFM7rxUD.js.br +0 -0
  236. package/payload/server/public/assets/useMediaQuery-fFM7rxUD.js.gz +0 -0
  237. package/payload/server/public/assets/useVoiceRecorder-D0MWFIde.js.br +0 -0
  238. package/payload/server/public/assets/useVoiceRecorder-D0MWFIde.js.gz +0 -0
  239. package/payload/server/public/assets/wrench-C-yn7q1Q.js.br +0 -0
  240. package/payload/server/public/assets/wrench-C-yn7q1Q.js.gz +0 -0
  241. /package/payload/server/public/assets/{jsx-runtime-DXWyHqht.js → jsx-runtime-BcrVQeV-.js} +0 -0
  242. /package/payload/server/public/assets/{jsx-runtime-DXWyHqht.js.br → jsx-runtime-BcrVQeV-.js.br} +0 -0
  243. /package/payload/server/public/assets/{jsx-runtime-DXWyHqht.js.gz → jsx-runtime-BcrVQeV-.js.gz} +0 -0
@@ -13,13 +13,6 @@ const runCalls = [];
13
13
  const writeCalls = [];
14
14
  /** What the pre-read reports the stored row holds; per-test. */
15
15
  let storedRow = {};
16
- /** Task 2634 — what the commit-action read finds on the account; per-test. The
17
- * two reads share one mocked session, so they are told apart by the only thing
18
- * that distinguishes them at the driver: the query text. */
19
- let commitActionRows = [];
20
- function isCommitActionRead(query) {
21
- return query.includes("actionTool: 'scheduling-gate'");
22
- }
23
16
  vi.mock("../../lib/embedding.js", () => ({
24
17
  computeEmbedding: vi.fn(async () => []),
25
18
  }));
@@ -27,13 +20,6 @@ vi.mock("../../lib/neo4j.js", () => ({
27
20
  getSession: () => ({
28
21
  run: vi.fn(async (query, params) => {
29
22
  runCalls.push({ query, params });
30
- if (isCommitActionRead(query)) {
31
- return {
32
- records: commitActionRows.map((row) => ({
33
- get: (k) => row[k] ?? null,
34
- })),
35
- };
36
- }
37
23
  return {
38
24
  records: [
39
25
  {
@@ -50,9 +36,35 @@ vi.mock("../../../../../../lib/graph-write/dist/index.js", () => ({
50
36
  writeCalls.push(props);
51
37
  }),
52
38
  }));
39
+ /** Task 2658 — what the bind-time probe reports, per-test. The real prober is
40
+ * covered by `scheduling-gate.test.ts`; this file owns the GUARD, so the probe
41
+ * is stubbed and its call arguments are asserted instead. Without the stub the
42
+ * pre-existing `mail`-gate tests below would open a live IMAP connection.
43
+ *
44
+ * `gateHasWork` is a SEPARATE knob from `gateErrored` on purpose. Deriving one
45
+ * from the other would make "the sources answered, work pending" and "the
46
+ * sources answered, nothing pending" the same fixture, and a guard that
47
+ * wrongly keys on `hasWork` would pass both. */
48
+ let gateErrored = [];
49
+ let gateHasWork = false;
50
+ const gateRunCalls = [];
51
+ vi.mock("../scheduling-gate.js", async (importOriginal) => {
52
+ const actual = await importOriginal();
53
+ return {
54
+ ...actual,
55
+ runSchedulingGate: vi.fn(async (params) => {
56
+ gateRunCalls.push(params);
57
+ return {
58
+ hasWork: gateHasWork || gateErrored.length > 0,
59
+ reasons: gateHasWork ? ["d1"] : [],
60
+ errored: gateErrored,
61
+ };
62
+ }),
63
+ };
64
+ });
53
65
  import { scheduleUpdate } from "../schedule-update.js";
54
66
  import { scheduleEvent } from "../schedule-event.js";
55
- import { validateGate, hasCommitAction } from "../schedule-event.js";
67
+ import { validateGate, probeGateAtBindTime, assertGateProbed } from "../schedule-event.js";
56
68
  const ACCOUNT_ID = "11111111-1111-4111-8111-111111111111";
57
69
  const PHONE = "+447700900001";
58
70
  const GATE = { plugin: "scheduling", tool: "scheduling-gate", args: { eventId: "e1", checks: [] } };
@@ -70,7 +82,9 @@ beforeEach(() => {
70
82
  runCalls.length = 0;
71
83
  writeCalls.length = 0;
72
84
  storedRow = {};
73
- commitActionRows = [];
85
+ gateErrored = [];
86
+ gateHasWork = false;
87
+ gateRunCalls.length = 0;
74
88
  const root = mkdtempSync(join(tmpdir(), "sched-gate-write-"));
75
89
  platformRoot = join(root, "platform");
76
90
  mkdirSync(platformRoot, { recursive: true });
@@ -85,17 +99,17 @@ afterEach(() => {
85
99
  });
86
100
  describe("validateGate", () => {
87
101
  it("accepts a gate on a complete agent dispatch", () => {
88
- expect(() => validateGate(GATE, { hasAction: false, hasCompleteAgentDispatch: true, resultingPrompt: null, eventId: null, hasCommitAction: false })).not.toThrow();
102
+ expect(() => validateGate(GATE, { hasAction: false, hasCompleteAgentDispatch: true, eventId: null })).not.toThrow();
89
103
  });
90
104
  it("refuses a gate on an action-mode event", () => {
91
- expect(() => validateGate(GATE, { hasAction: true, hasCompleteAgentDispatch: false, resultingPrompt: null, eventId: null, hasCommitAction: false })).toThrow(/action already runs headlessly/);
105
+ expect(() => validateGate(GATE, { hasAction: true, hasCompleteAgentDispatch: false, eventId: null })).toThrow(/action already runs headlessly/);
92
106
  });
93
107
  it("refuses a gate with no complete agent dispatch", () => {
94
- expect(() => validateGate(GATE, { hasAction: false, hasCompleteAgentDispatch: false, resultingPrompt: null, eventId: null, hasCommitAction: false })).toThrow(/requires a complete agentDispatch/);
108
+ expect(() => validateGate(GATE, { hasAction: false, hasCompleteAgentDispatch: false, eventId: null })).toThrow(/requires a complete agentDispatch/);
95
109
  });
96
110
  it("refuses a gate missing plugin or tool", () => {
97
- expect(() => validateGate({ plugin: "", tool: "t" }, { hasAction: false, hasCompleteAgentDispatch: true, resultingPrompt: null, eventId: null, hasCommitAction: false })).toThrow(/both 'plugin' and 'tool'/);
98
- expect(() => validateGate({ plugin: "p", tool: "" }, { hasAction: false, hasCompleteAgentDispatch: true, resultingPrompt: null, eventId: null, hasCommitAction: false })).toThrow(/both 'plugin' and 'tool'/);
111
+ expect(() => validateGate({ plugin: "", tool: "t" }, { hasAction: false, hasCompleteAgentDispatch: true, eventId: null })).toThrow(/both 'plugin' and 'tool'/);
112
+ expect(() => validateGate({ plugin: "p", tool: "" }, { hasAction: false, hasCompleteAgentDispatch: true, eventId: null })).toThrow(/both 'plugin' and 'tool'/);
99
113
  });
100
114
  });
101
115
  describe("scheduleEvent — gate", () => {
@@ -277,235 +291,10 @@ const UNWATERMARKED_GATE = {
277
291
  ],
278
292
  },
279
293
  };
280
- // Task 2634 — the seed substitutes `<<gateCommit>>` out of the prompt before it
281
- // is stored (`routine-templates/src/index.ts`), so a shipped routine's stored
282
- // prompt carries the expanded argument and never the token. Both spellings are
283
- // the same route and both are tested: accepting only the token would refuse
284
- // every seeded routine on its next gate edit.
285
- const TOKEN_PROMPT = "sweep, then call scheduling-gate with <<gateCommit>>";
286
- const COMMIT_PROMPT = 'sweep, then call scheduling-gate with {"eventId":"e1","checks":[{"type":"mail","mailbox":"ops@example.com"}],"commit":true}';
287
- /** The live shape Task 2634 was filed on: names the tool, forbids the call. */
288
- const FORBIDS_COMMIT_PROMPT = "sweep. Do NOT call scheduling-gate yourself. A separate routine advances the watermark.";
289
- /** A scheduled action event that commits this row's watermark with no model. */
290
- function commitActionRow(over = {}) {
291
- return {
292
- eventStatus: "scheduled",
293
- actionPlugin: "scheduling",
294
- actionTool: "scheduling-gate",
295
- actionArgs: JSON.stringify({
296
- eventId: "e1",
297
- checks: [{ type: "mail", mailbox: "ops@example.com" }],
298
- commit: true,
299
- }),
300
- ...over,
301
- };
302
- }
303
- describe("commitless watermarked gate", () => {
304
- it("is refused on update when the resulting prompt has no commit step", async () => {
305
- storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
306
- await expect(scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: MAIL_GATE })).rejects.toThrow(/scheduling-gate/);
307
- expect(writeCalls).toHaveLength(0);
308
- expect(lastSet()?.query ?? "").not.toContain("e.gatePlugin");
309
- });
310
- it("is accepted on update when the stored prompt carries the commit step", async () => {
311
- storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: COMMIT_PROMPT };
312
- await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: MAIL_GATE });
313
- expect(lastSet().query).toContain("e.gatePlugin");
314
- });
315
- it("reads the SUPPLIED prompt over the stored one", async () => {
316
- storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
317
- await scheduleUpdate({
318
- accountId: ACCOUNT_ID,
319
- eventId: "e1",
320
- gate: MAIL_GATE,
321
- agentDispatch: { channel: "whatsapp", destination: PHONE, prompt: COMMIT_PROMPT },
322
- });
323
- expect(lastSet().query).toContain("e.gatePlugin");
324
- });
325
- it("accepts an unwatermarked gate against a prompt with no commit step", async () => {
326
- storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
327
- await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: UNWATERMARKED_GATE });
328
- expect(lastSet().query).toContain("e.gatePlugin");
329
- });
330
- it("is refused on create too, because both paths share validateGate", () => {
331
- expect(() => validateGate(MAIL_GATE, {
332
- hasAction: false,
333
- hasCompleteAgentDispatch: true,
334
- resultingPrompt: "sweep",
335
- eventId: null,
336
- hasCommitAction: false,
337
- })).toThrow(/scheduling-gate/);
338
- expect(() => validateGate(MAIL_GATE, {
339
- hasAction: false,
340
- hasCompleteAgentDispatch: true,
341
- resultingPrompt: COMMIT_PROMPT,
342
- eventId: null,
343
- hasCommitAction: false,
344
- })).not.toThrow();
345
- });
346
- // The watermark file is a `scheduling-gate` concept, so the reason this
347
- // refusal exists does not apply to a different tool's arguments.
348
- it("does not refuse a gate belonging to another tool", () => {
349
- expect(() => validateGate({ plugin: "other", tool: "other-gate", args: { checks: [{ type: "mail", mailbox: "m" }] } }, {
350
- hasAction: false,
351
- hasCompleteAgentDispatch: true,
352
- resultingPrompt: "sweep",
353
- eventId: null,
354
- hasCommitAction: false,
355
- })).not.toThrow();
356
- });
357
- });
358
- // ---------------------------------------------------------------------------
359
- // Task 2634 — the guard above was a substring test on the prompt, so a prompt
360
- // reading "Do NOT call scheduling-gate yourself" satisfied a guard that exists
361
- // to prove the prompt calls it. Two routes replace it, and the refusal names
362
- // both: the prompt carries the commit argument, or a scheduled action-mode
363
- // event commits this row's watermark with no model in the path.
364
- // ---------------------------------------------------------------------------
365
- describe("commit route — prompt (route 1)", () => {
366
- function check(prompt, over = {}) {
367
- return () => validateGate(MAIL_GATE, {
368
- hasAction: false,
369
- hasCompleteAgentDispatch: true,
370
- resultingPrompt: prompt,
371
- eventId: "e1",
372
- hasCommitAction: false,
373
- ...over,
374
- });
375
- }
376
- it("refuses a prompt that names the tool only to forbid the call", () => {
377
- expect(check(FORBIDS_COMMIT_PROMPT)).toThrow(/scheduling-gate/);
378
- });
379
- it("names both routes in the refusal", () => {
380
- expect(check(FORBIDS_COMMIT_PROMPT)).toThrow(/"commit":true/);
381
- expect(check(FORBIDS_COMMIT_PROMPT)).toThrow(/action-mode event/);
382
- });
383
- it("accepts the unexpanded roster token", () => {
384
- expect(check(TOKEN_PROMPT)).not.toThrow();
385
- });
386
- it("accepts the seeded prompt, whose token the seed already expanded", () => {
387
- expect(check(COMMIT_PROMPT)).not.toThrow();
388
- });
389
- it("accepts the commit argument written with a space after the key", () => {
390
- expect(check('call scheduling-gate with {"eventId":"e1", "commit": true}')).not.toThrow();
391
- });
392
- it("refuses prose that says commit without carrying the argument", () => {
393
- expect(check("sweep, then commit the watermark with scheduling-gate")).toThrow(/scheduling-gate/);
394
- });
395
- it("leaves an unwatermarked gate alone whatever the prompt says", () => {
396
- expect(() => validateGate(UNWATERMARKED_GATE, {
397
- hasAction: false,
398
- hasCompleteAgentDispatch: true,
399
- resultingPrompt: FORBIDS_COMMIT_PROMPT,
400
- eventId: "e1",
401
- hasCommitAction: false,
402
- })).not.toThrow();
403
- });
404
- });
405
- describe("commit route — bound action event (route 2)", () => {
406
- it("accepts a watermarked gate whose commit is a separate action event", () => {
407
- expect(() => validateGate(MAIL_GATE, {
408
- hasAction: false,
409
- hasCompleteAgentDispatch: true,
410
- resultingPrompt: FORBIDS_COMMIT_PROMPT,
411
- eventId: "e1",
412
- hasCommitAction: true,
413
- })).not.toThrow();
414
- });
415
- describe("hasCommitAction", () => {
416
- it("matches a scheduled scheduling-gate action naming this event with commit true", () => {
417
- expect(hasCommitAction([commitActionRow()], "e1")).toBe(true);
418
- });
419
- it("rejects commit false", () => {
420
- const args = JSON.parse(commitActionRow().actionArgs);
421
- args.commit = false;
422
- expect(hasCommitAction([commitActionRow({ actionArgs: JSON.stringify(args) })], "e1")).toBe(false);
423
- });
424
- it("rejects a different eventId", () => {
425
- expect(hasCommitAction([commitActionRow()], "e2")).toBe(false);
426
- });
427
- it("rejects a row whose eventStatus is not scheduled", () => {
428
- expect(hasCommitAction([commitActionRow({ eventStatus: "suspended" })], "e1")).toBe(false);
429
- expect(hasCommitAction([commitActionRow({ eventStatus: "cancelled" })], "e1")).toBe(false);
430
- });
431
- it("rejects a row bound to another tool", () => {
432
- expect(hasCommitAction([commitActionRow({ actionTool: "schedule-list" })], "e1")).toBe(false);
433
- expect(hasCommitAction([commitActionRow({ actionPlugin: "work" })], "e1")).toBe(false);
434
- });
435
- // A row whose args do not parse is a non-match, not a throw: the validator's
436
- // job is to refuse cleanly, and one malformed row must not take down the
437
- // write path for every other row on the account.
438
- it("treats unparseable or absent args as a non-match", () => {
439
- expect(hasCommitAction([commitActionRow({ actionArgs: "{not json" })], "e1")).toBe(false);
440
- expect(hasCommitAction([commitActionRow({ actionArgs: null })], "e1")).toBe(false);
441
- expect(hasCommitAction([commitActionRow({ actionArgs: "[1,2]" })], "e1")).toBe(false);
442
- });
443
- // At create time the id is minted after the validator runs, so there is no
444
- // id for an action event to have named. Route 2 is unreachable on that path
445
- // by construction, and a null id must never match a row.
446
- it("never matches when the row has no id yet", () => {
447
- expect(hasCommitAction([commitActionRow()], null)).toBe(false);
448
- });
449
- it("is false on an empty account", () => {
450
- expect(hasCommitAction([], "e1")).toBe(false);
451
- });
452
- // The row Task 2634 was filed on, read from laptop 192.168.88.10 SiteDesk
453
- // account 5e99bba5 on 2026-08-09. Verbatim, because the stub above chose its
454
- // own key order and the live writer chose a different one: this is the only
455
- // assertion that the shape the fleet actually stores parses the same way.
456
- // Its routine's prompt carries neither spelling of the commit argument
457
- // (measured: `agentPrompt CONTAINS '"commit":true'` and `CONTAINS
458
- // '<<gateCommit>>'` are both FALSE, while the old substring test's
459
- // `CONTAINS 'scheduling-gate'` is TRUE), so route 2 alone is what keeps it
460
- // valid on its next edit.
461
- it("matches the live SiteDesk commit action verbatim", () => {
462
- const live = {
463
- eventStatus: "scheduled",
464
- actionPlugin: "scheduling",
465
- actionTool: "scheduling-gate",
466
- actionArgs: '{"eventId":"14414285-8549-4ce1-907d-0d9cadd370ee","commit":true,"checks":[{"type":"mail","mailbox":"hello@beagle.direct","folder":"inbox"},{"type":"mail","mailbox":"operations@sitedesk.online","folder":"inbox"}]}',
467
- };
468
- expect(hasCommitAction([live], "14414285-8549-4ce1-907d-0d9cadd370ee")).toBe(true);
469
- expect(hasCommitAction([live], "ae361343-2d85-426d-8eb3-fde621c8cce0")).toBe(false);
470
- });
471
- });
472
- });
473
- describe("scheduleUpdate — commit routes end to end", () => {
474
- beforeEach(() => {
475
- storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: FORBIDS_COMMIT_PROMPT };
476
- });
477
- it("refuses when neither route is taken", async () => {
478
- await expect(scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: MAIL_GATE })).rejects.toThrow(/scheduling-gate/);
479
- expect(lastSet()?.query ?? "").not.toContain("e.gatePlugin");
480
- });
481
- it("accepts when a bound action event commits the watermark", async () => {
482
- commitActionRows = [commitActionRow()];
483
- await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: MAIL_GATE });
484
- expect(lastSet().query).toContain("e.gatePlugin");
485
- });
486
- it("refuses when the bound action event commits nothing", async () => {
487
- const args = JSON.parse(commitActionRow().actionArgs);
488
- args.commit = false;
489
- commitActionRows = [commitActionRow({ actionArgs: JSON.stringify(args) })];
490
- await expect(scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: MAIL_GATE })).rejects.toThrow(/scheduling-gate/);
491
- });
492
- it("scopes the commit-action read to the calling account", async () => {
493
- commitActionRows = [commitActionRow()];
494
- await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: MAIL_GATE });
495
- const read = runCalls.find((c) => isCommitActionRead(c.query));
496
- expect(read?.params.accountId).toBe(ACCOUNT_ID);
497
- });
498
- // The read costs a round trip, so it runs only for the gates that have a
499
- // commit route to look for.
500
- it("does not read commit actions for an unwatermarked gate", async () => {
501
- await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: UNWATERMARKED_GATE });
502
- expect(runCalls.some((c) => isCommitActionRead(c.query))).toBe(false);
503
- });
504
- });
505
- // A guard that refuses silently is indistinguishable from one that never ran,
506
- // and an acceptance that does not name its route is indistinguishable from a
507
- // skipped guard. Both lines carry the check types, so a refusal can be read
508
- // without going back to the row.
294
+ // Task 2663 — the three prompt fixtures are gone with the guard that read them.
295
+ // A prompt is no longer any part of whether a gate is accepted: the platform
296
+ // advances the mark on every accepted wake, so there is nothing for a prompt to
297
+ // arrange and nothing to inspect it for.
509
298
  describe("gate decision evidence", () => {
510
299
  let lines;
511
300
  beforeEach(() => {
@@ -519,9 +308,7 @@ describe("gate decision evidence", () => {
519
308
  validateGate(MAIL_GATE, {
520
309
  hasAction: false,
521
310
  hasCompleteAgentDispatch: true,
522
- resultingPrompt: FORBIDS_COMMIT_PROMPT,
523
311
  eventId: "e1",
524
- hasCommitAction: false,
525
312
  ...over,
526
313
  });
527
314
  }
@@ -530,30 +317,18 @@ describe("gate decision evidence", () => {
530
317
  }
531
318
  return lines.filter((l) => l.includes("op=gate-"));
532
319
  }
533
- it("emits one refusal line naming the reason and the checks", () => {
534
- expect(decide({})).toEqual(["[schedule-authoring] op=gate-refused eventId=e1 reason=no-commit-route checks=mail"]);
535
- });
536
- it("emits route=token when the prompt carries the commit", () => {
537
- expect(decide({ resultingPrompt: COMMIT_PROMPT })).toEqual([
538
- "[schedule-authoring] op=gate-accepted eventId=e1 route=token checks=mail",
539
- ]);
540
- });
541
- it("emits route=action-event when a bound action carries it", () => {
542
- expect(decide({ hasCommitAction: true })).toEqual([
543
- "[schedule-authoring] op=gate-accepted eventId=e1 route=action-event checks=mail",
544
- ]);
545
- });
546
- it("carries a dash for the id the create path has not minted yet", () => {
547
- expect(decide({ eventId: null })[0]).toContain("eventId=-");
320
+ it("emits no decision line for a watermarked gate, which needs no arrangement (Task 2663)", () => {
321
+ // The refusal this block was written for is deleted: the platform advances
322
+ // the mark on every accepted wake, so a watermarked check is no longer a
323
+ // reason to refuse a write and there is no route to name.
324
+ expect(decide({})).toEqual([]);
548
325
  });
549
326
  it("says nothing for a gate with no commit route to take", () => {
550
327
  lines = [];
551
328
  validateGate(UNWATERMARKED_GATE, {
552
329
  hasAction: false,
553
330
  hasCompleteAgentDispatch: true,
554
- resultingPrompt: "sweep",
555
331
  eventId: "e1",
556
- hasCommitAction: false,
557
332
  });
558
333
  expect(lines.filter((l) => l.includes("op=gate-"))).toEqual([]);
559
334
  });
@@ -576,9 +351,7 @@ const gateWith = (checks) => ({
576
351
  const ctx = (over = {}) => ({
577
352
  hasAction: false,
578
353
  hasCompleteAgentDispatch: true,
579
- resultingPrompt: COMMIT_PROMPT,
580
354
  eventId: "e1",
581
- hasCommitAction: false,
582
355
  ...over,
583
356
  });
584
357
  describe("unknown gate check type", () => {
@@ -613,11 +386,12 @@ describe("unknown gate check type", () => {
613
386
  it("accepts a telegram check", () => {
614
387
  expect(() => validateGate(gateWith([{ type: "telegram", scope: "inbound" }]), ctx())).not.toThrow();
615
388
  });
616
- it("treats a telegram check as watermarked, so a commitless one is refused", () => {
617
- // It compares against a stored position exactly as mail and calendar do, so
618
- // without a commit it reports work on every fire and saves nothing.
389
+ it("accepts a telegram check with no commit arrangement (Task 2663)", () => {
390
+ // It compares against a stored position exactly as mail and calendar do, and
391
+ // the dispatcher advances that position itself now, so nothing about the
392
+ // prompt is any longer a reason to refuse the write.
619
393
  const gate = gateWith([{ type: "telegram", scope: "inbound" }]);
620
- expect(() => validateGate(gate, ctx({ resultingPrompt: "sweep" }))).toThrow(/scheduling-gate/);
394
+ expect(() => validateGate(gate, ctx())).not.toThrow();
621
395
  });
622
396
  it("leaves a gate on another plugin's tool alone", () => {
623
397
  // The accepted set is this tool's own vocabulary. A gate pointing at some
@@ -626,4 +400,294 @@ describe("unknown gate check type", () => {
626
400
  expect(() => validateGate(foreign, ctx())).not.toThrow();
627
401
  });
628
402
  });
403
+ // Task 2657 — a mail provider the prober cannot answer, refused at write time.
404
+ //
405
+ // Same reasoning as the unknown check type above: stored, it throws on every
406
+ // fire, the dispatcher fails OPEN, and the routine spends a full model turn
407
+ // while every surface reads "gated".
408
+ describe("unknown mail provider", () => {
409
+ let refusalLines;
410
+ beforeEach(() => {
411
+ refusalLines = [];
412
+ vi.spyOn(console, "error").mockImplementation((line) => {
413
+ refusalLines.push(String(line));
414
+ });
415
+ });
416
+ const mailGate = (provider) => gateWith([
417
+ provider === undefined
418
+ ? { type: "mail", mailbox: "a@b.com" }
419
+ : { type: "mail", mailbox: "a@b.com", provider },
420
+ ]);
421
+ it("refuses a provider outside the accepted list", () => {
422
+ expect(() => validateGate(mailGate("pop3"), ctx())).toThrow(/provider not recognised/i);
423
+ });
424
+ it("names the provider and the accepted set on the refusal line", () => {
425
+ expect(() => validateGate(mailGate("pop3"), ctx())).toThrow();
426
+ const line = refusalLines.find((l) => l.includes("reason=unknown-mail-provider"));
427
+ expect(line).toContain("providers=pop3");
428
+ });
429
+ it("accepts outlook", () => {
430
+ expect(() => validateGate(mailGate("outlook"), ctx())).not.toThrow();
431
+ });
432
+ it("accepts imap named explicitly", () => {
433
+ expect(() => validateGate(mailGate("imap"), ctx())).not.toThrow();
434
+ });
435
+ it("accepts a mail check carrying no provider at all", () => {
436
+ expect(() => validateGate(mailGate(), ctx())).not.toThrow();
437
+ });
438
+ it("refuses a provider on a check that is not mail", () => {
439
+ // Task 2662 REVERSES Task 2657's decision here, deliberately.
440
+ //
441
+ // 2657 allowed a stray `provider` on a non-mail check, reasoning that it is
442
+ // the mail check's own field and refusing it "would reject a valid gate". A
443
+ // gate carrying a field nothing reads is not valid, it is misleading: that
444
+ // is the exact defect an operator reported against 0.1.590, having passed a
445
+ // `botId` to a telegram check and watched it report work with the field
446
+ // discarded. `provider` on a d1 check is the same class.
447
+ const gate = gateWith([{ type: "d1", database: "b", sql: "SELECT 1", provider: "pop3" }]);
448
+ expect(() => validateGate(gate, ctx())).toThrow(/provider/);
449
+ });
450
+ });
451
+ // ---------------------------------------------------------------------------
452
+ // Task 2662 — a field the prober never reads.
453
+ //
454
+ // `validateGate` judged the check's `type`, its `provider` and the commit route,
455
+ // and nothing else. `checks` is `z.array(z.record(z.unknown()))`, so every other
456
+ // key passed and was dropped on the floor. The operator reproduced it twice: a
457
+ // real bot id on a telegram check reported work, then `0000000000` reported work,
458
+ // because neither reached anything that read them.
459
+ // ---------------------------------------------------------------------------
460
+ describe("unknown gate check field", () => {
461
+ let fieldLines;
462
+ beforeEach(() => {
463
+ fieldLines = [];
464
+ vi.spyOn(console, "error").mockImplementation((line) => {
465
+ fieldLines.push(String(line));
466
+ });
467
+ });
468
+ it("is refused, naming the field and the keys that type accepts", () => {
469
+ const gate = gateWith([{ type: "telegram", scope: "inbound", nonsense: 1 }]);
470
+ expect(() => validateGate(gate, ctx())).toThrow(/nonsense/);
471
+ expect(() => validateGate(gate, ctx())).toThrow(/type.*scope.*botId/);
472
+ });
473
+ it("logs op=gate-refused so the refusal is greppable", () => {
474
+ try {
475
+ validateGate(gateWith([{ type: "telegram", scope: "inbound", nonsense: 1 }]), ctx());
476
+ }
477
+ catch {
478
+ /* asserted above */
479
+ }
480
+ const line = fieldLines.find((l) => l.includes("op=gate-refused"));
481
+ expect(line).toContain("reason=unknown-check-field");
482
+ expect(line).toContain("telegram:nonsense");
483
+ });
484
+ it("accepts botId on a telegram check", () => {
485
+ const gate = gateWith([{ type: "telegram", scope: "inbound", botId: "111" }]);
486
+ expect(() => validateGate(gate, ctx())).not.toThrow();
487
+ });
488
+ it("refuses a telegram scope other than inbound", () => {
489
+ // `scope` was decorative: probe()'s telegram arm never read it, so any
490
+ // value passed and behaved as `inbound`.
491
+ const gate = gateWith([{ type: "telegram", scope: "status" }]);
492
+ expect(() => validateGate(gate, ctx())).toThrow(/scope/);
493
+ });
494
+ it("still accepts every shape the four other checks legitimately carry", () => {
495
+ const gate = gateWith([
496
+ { type: "mail", mailbox: "ops@example.com", folder: "INBOX", subjectPrefixes: ["RE:"] },
497
+ { type: "mail", mailbox: "ops@example.com", provider: "outlook" },
498
+ { type: "d1", database: "b", sql: "SELECT count(*) FROM b" },
499
+ { type: "whatsapp", scope: "status" },
500
+ { type: "calendar", connector: "google" },
501
+ ]);
502
+ expect(() => validateGate(gate, ctx())).not.toThrow();
503
+ });
504
+ });
505
+ // ---------------------------------------------------------------------------
506
+ // Task 2658 — the four refusals above all judge the gate's SHAPE. A gate naming
507
+ // an unregistered database, a mailbox whose host does not resolve, or an
508
+ // account whose socket is down is well-formed JSON and passes every one of
509
+ // them. It then stores cleanly, throws inside the tool on each fire, and the
510
+ // dispatcher fails OPEN: two muvin routines cost 96 and 95 model turns in 24
511
+ // hours that way while `schedule-list` and `/routines` both read "gated".
512
+ // ---------------------------------------------------------------------------
513
+ const D1_GATE = {
514
+ plugin: "scheduling",
515
+ tool: "scheduling-gate",
516
+ args: { eventId: "e1", checks: [{ type: "d1", database: "unregistered", sql: "SELECT 1" }] },
517
+ };
518
+ describe("probeGateAtBindTime", () => {
519
+ it("runs the gate exactly once, and the gate writes nothing (Task 2663)", async () => {
520
+ // `commit` is deleted, so a bind-time probe cannot leave a mark behind for a
521
+ // row that does not exist yet: the gate has no write path at all.
522
+ await probeGateAtBindTime(D1_GATE, ACCOUNT_ID, "e1");
523
+ expect(gateRunCalls).toHaveLength(1);
524
+ expect(gateRunCalls[0]).not.toHaveProperty("commit");
525
+ });
526
+ it("passes the row's own id on update, so a watermarked check reads its real position", async () => {
527
+ await probeGateAtBindTime(D1_GATE, ACCOUNT_ID, "e1");
528
+ expect(gateRunCalls[0].eventId).toBe("e1");
529
+ });
530
+ it("passes no id on create, so checks read position zero", async () => {
531
+ await probeGateAtBindTime(D1_GATE, ACCOUNT_ID, null);
532
+ expect(gateRunCalls[0].eventId).toBe("");
533
+ });
534
+ it("returns null for a gate belonging to another tool, and probes nothing", async () => {
535
+ const probe = await probeGateAtBindTime({ plugin: "other", tool: "other-gate", args: { checks: [{ type: "d1" }] } }, ACCOUNT_ID, "e1");
536
+ expect(probe).toBeNull();
537
+ expect(gateRunCalls).toHaveLength(0);
538
+ });
539
+ it("reports every distinct kind the gate carries", async () => {
540
+ const probe = await probeGateAtBindTime({
541
+ plugin: "scheduling",
542
+ tool: "scheduling-gate",
543
+ args: {
544
+ checks: [
545
+ { type: "d1", database: "db", sql: "SELECT 1" },
546
+ { type: "d1", database: "db2", sql: "SELECT 1" },
547
+ { type: "whatsapp", scope: "status" },
548
+ ],
549
+ },
550
+ }, ACCOUNT_ID, "e1");
551
+ expect(probe?.kinds).toEqual(["d1", "whatsapp"]);
552
+ });
553
+ it("passes the errored list straight through", async () => {
554
+ gateErrored = [{ kind: "d1", detail: "HTTP 403" }];
555
+ const probe = await probeGateAtBindTime(D1_GATE, ACCOUNT_ID, "e1");
556
+ expect(probe?.errored).toEqual([{ kind: "d1", detail: "HTTP 403" }]);
557
+ });
558
+ });
559
+ describe("assertGateProbed", () => {
560
+ /** The lines the guard wrote, joined. */
561
+ function captured(fn) {
562
+ const lines = [];
563
+ const spy = vi.spyOn(console, "error").mockImplementation((...a) => {
564
+ lines.push(a.join(" "));
565
+ });
566
+ try {
567
+ fn();
568
+ }
569
+ finally {
570
+ spy.mockRestore();
571
+ }
572
+ return lines.join("\n");
573
+ }
574
+ it("does nothing at all when the gate was not probed", () => {
575
+ let threw = false;
576
+ const out = captured(() => {
577
+ try {
578
+ assertGateProbed(null, "e1");
579
+ }
580
+ catch {
581
+ threw = true;
582
+ }
583
+ });
584
+ expect(threw).toBe(false);
585
+ expect(out).toBe("");
586
+ });
587
+ it("accepts a gate whose checks all answered, and says so", () => {
588
+ const out = captured(() => assertGateProbed({ errored: [], kinds: ["d1", "mail"] }, "e1"));
589
+ expect(out).toContain("op=gate-accepted eventId=e1 route=probed checks=d1,mail");
590
+ });
591
+ it("refuses a gate whose check could not answer, carrying the source's own text", () => {
592
+ let message = "";
593
+ const out = captured(() => {
594
+ try {
595
+ assertGateProbed({
596
+ errored: [{ kind: "d1", detail: "HTTP 403 on POST /api/storage/d1/query" }],
597
+ kinds: ["d1"],
598
+ }, "e1");
599
+ }
600
+ catch (err) {
601
+ message = err instanceof Error ? err.message : String(err);
602
+ }
603
+ });
604
+ expect(message).toContain("HTTP 403 on POST /api/storage/d1/query");
605
+ expect(out).toContain("op=gate-refused eventId=e1 reason=check-unreachable checks=d1");
606
+ });
607
+ // The refusal names the kinds that FAILED, as `reason=unknown-check-type`
608
+ // names only the offending types: that is the set the author has to fix.
609
+ it("names only the kinds that errored in the refusal line", () => {
610
+ const out = captured(() => {
611
+ try {
612
+ assertGateProbed({ errored: [{ kind: "mail", detail: "getaddrinfo EAI_AGAIN" }], kinds: ["d1", "mail"] }, "e1");
613
+ }
614
+ catch {
615
+ /* asserted by the line, not the throw */
616
+ }
617
+ });
618
+ expect(out).toContain("reason=check-unreachable checks=mail");
619
+ });
620
+ it("writes '-' for the id on the create path, where none is minted yet", () => {
621
+ const out = captured(() => assertGateProbed({ errored: [], kinds: ["d1"] }, null));
622
+ expect(out).toContain("eventId=- route=probed");
623
+ });
624
+ });
625
+ // Both paths asserted separately. One path guarded is the drift
626
+ // `gateRequiresCommitRoute`'s docstring exists to prevent.
627
+ describe("unreachable gate — write paths", () => {
628
+ const createBase = {
629
+ accountId: ACCOUNT_ID,
630
+ name: "Quarter-hourly sweep",
631
+ startDate: "2026-08-03T08:00:00.000Z",
632
+ recurrence: "*/15 * * * *",
633
+ sessionKey: "sess-1",
634
+ createdBy: { agent: "admin", session: "sess-1", tool: "schedule-event" },
635
+ };
636
+ it("is refused on create, carrying the source's own text", async () => {
637
+ gateErrored = [{ kind: "d1", detail: "HTTP 403 on POST /api/storage/d1/query" }];
638
+ await expect(scheduleEvent({ ...createBase, agentDispatch: DISPATCH, gate: D1_GATE })).rejects.toThrow(/HTTP 403 on POST \/api\/storage\/d1\/query/);
639
+ expect(writeCalls).toHaveLength(0);
640
+ });
641
+ it("is refused on update, carrying the source's own text", async () => {
642
+ gateErrored = [{ kind: "d1", detail: "HTTP 403 on POST /api/storage/d1/query" }];
643
+ storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
644
+ await expect(scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: D1_GATE })).rejects.toThrow(/HTTP 403 on POST \/api\/storage\/d1\/query/);
645
+ expect(lastSet()?.query ?? "").not.toContain("e.gatePlugin");
646
+ });
647
+ // `hasWork` is NOT judged. A guard keying on it passes the false case and
648
+ // silently refuses every account that happens to be busy at bind time, so
649
+ // both answers are asserted and the two fixtures differ only in `hasWork`.
650
+ it("is stored when the checks answer 'work pending'", async () => {
651
+ gateErrored = [];
652
+ gateHasWork = true;
653
+ storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
654
+ await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: D1_GATE });
655
+ expect(lastSet().query).toContain("e.gatePlugin");
656
+ });
657
+ it("is stored when the checks answer 'nothing pending'", async () => {
658
+ gateErrored = [];
659
+ gateHasWork = false;
660
+ storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
661
+ await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: D1_GATE });
662
+ expect(lastSet().query).toContain("e.gatePlugin");
663
+ });
664
+ it("probes with the row's own id on update", async () => {
665
+ storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
666
+ await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: D1_GATE });
667
+ expect(gateRunCalls[0].eventId).toBe("e1");
668
+ expect(gateRunCalls[0]).not.toHaveProperty("commit");
669
+ });
670
+ it("probes with no id on create", async () => {
671
+ await scheduleEvent({ ...createBase, agentDispatch: DISPATCH, gate: D1_GATE });
672
+ expect(gateRunCalls[0].eventId).toBe("");
673
+ expect(gateRunCalls[0]).not.toHaveProperty("commit");
674
+ });
675
+ // The ordering rule the 2471 refusal already states: a caller with no
676
+ // dispatch is told about the dispatch, not about a source it cannot reach.
677
+ it("tells a caller with no agent dispatch about the dispatch first", async () => {
678
+ gateErrored = [{ kind: "d1", detail: "HTTP 403" }];
679
+ await expect(scheduleEvent({ ...createBase, gate: D1_GATE })).rejects.toThrow(/requires a complete agentDispatch/);
680
+ expect(gateRunCalls).toHaveLength(0);
681
+ });
682
+ it("does not probe a gate belonging to another tool", async () => {
683
+ storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
684
+ await scheduleUpdate({
685
+ accountId: ACCOUNT_ID,
686
+ eventId: "e1",
687
+ gate: { plugin: "other", tool: "other-gate", args: { checks: [{ type: "d1" }] } },
688
+ });
689
+ expect(gateRunCalls).toHaveLength(0);
690
+ expect(lastSet().query).toContain("e.gatePlugin");
691
+ });
692
+ });
629
693
  //# sourceMappingURL=schedule-gate-write.test.js.map