kennel 0.2.3 → 0.2.6

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 (274) hide show
  1. package/README.md +23 -13
  2. package/dist/cli/dispatch.js +16 -3
  3. package/dist/cli/src/cli/dispatch.js +16 -3
  4. package/dist/cli/src/cli/usage.js +1 -1
  5. package/dist/cli/src/core/command.js +1 -0
  6. package/dist/cli/src/core/init.js +5 -5
  7. package/dist/cli/src/plugins/external.d.ts +28 -2
  8. package/dist/cli/src/plugins/external.js +77 -23
  9. package/dist/cli/src/site/server-plugins.d.ts +4 -9
  10. package/dist/cli/src/site/viewer.js +1 -0
  11. package/dist/cli/usage.js +1 -1
  12. package/dist/core/api-client.js +576 -10317
  13. package/dist/core/command.js +1 -0
  14. package/dist/core/core-folders.js +2313 -12088
  15. package/dist/core/init.js +5 -5
  16. package/dist/core/runtime.js +8237 -20713
  17. package/dist/package/preview.js +19617 -33352
  18. package/dist/plugins/external.d.ts +28 -2
  19. package/dist/plugins/external.js +77 -23
  20. package/dist/site/server-plugins.d.ts +4 -9
  21. package/dist/site/server-plugins.js +557 -10452
  22. package/dist/site/viewer.js +1 -0
  23. package/kernel/apps/web/package.json +2 -6
  24. package/kernel/apps/web/scripts/dev.mjs +53 -0
  25. package/kernel/apps/web/src/app.css +0 -3
  26. package/kernel/apps/web/src/external-plugins.ts +1 -1
  27. package/kernel/apps/web/src/folder-filter-icons.tsx +1 -1
  28. package/kernel/apps/web/src/folder-plugin-view.tsx +1 -1
  29. package/kernel/apps/web/src/main.tsx +0 -1
  30. package/kernel/apps/web/src/plugins.ts +1 -31
  31. package/kernel/apps/web/tsconfig.json +1 -2
  32. package/kernel/apps/web/vite.config.ts +32 -14
  33. package/kernel/packages/agents/package.json +1 -1
  34. package/kernel/packages/agents/src/agent-card.tsx +1 -1
  35. package/kernel/packages/agents/src/client.tsx +1 -1
  36. package/kernel/packages/agents/src/manifest.ts +1 -1
  37. package/kernel/packages/agents/src/server.ts +1 -1
  38. package/kernel/packages/agents/src/viewer.tsx +1 -1
  39. package/kernel/packages/api/package.json +1 -5
  40. package/kernel/packages/api/src/server/acp-live-plugin.ts +1 -1
  41. package/kernel/packages/api/src/server/api/create-agent-file.ts +2 -2
  42. package/kernel/packages/api/src/server/api/document-edit.ts +2 -2
  43. package/kernel/packages/api/src/server/api/open-path.ts +2 -2
  44. package/kernel/packages/api/src/server/folder-metadata-plugin.ts +17 -42
  45. package/kernel/packages/api/src/server/index.ts +0 -1
  46. package/kernel/packages/api/src/server/plugins.ts +52 -19
  47. package/kernel/packages/api/src/shared/viewer-route.ts +1 -1
  48. package/kernel/packages/api/test/core.test.ts +14 -26
  49. package/kernel/packages/markdown/README.md +1 -1
  50. package/kernel/packages/notes/package.json +1 -1
  51. package/kernel/packages/notes/src/client.tsx +1 -1
  52. package/kernel/packages/notes/src/manifest.ts +1 -1
  53. package/kernel/packages/notes/src/server.ts +1 -1
  54. package/kernel/packages/plugins/SKILL.md +39 -0
  55. package/kernel/{plugins/mail → packages/plugins}/package.json +53 -28
  56. package/kernel/packages/plugins/src/index.ts +52 -0
  57. package/kernel/packages/plugins/src/page.ts +17 -0
  58. package/kernel/{plugins/projects → packages/plugins}/tsconfig.json +5 -1
  59. package/kernel/packages/plugins/tsup.config.ts +25 -0
  60. package/kernel/packages/ui/src/components/content-issues.tsx +1 -1
  61. package/kernel/packages/ui/tsconfig.json +1 -1
  62. package/package.json +2 -3
  63. package/web/assets/{abnfDiagram-VRR7QNED-Bgfb1nEB.js → abnfDiagram-VRR7QNED-xd_g1uK3.js} +1 -1
  64. package/web/assets/app-CX20nNIa.js +2 -0
  65. package/web/assets/{app-options-D-RJiQ-X.js → app-options-CkPJE5ep.js} +1 -1
  66. package/web/assets/{app-view-header-BLvLq02T.js → app-view-header-CNk5KnA-.js} +1 -1
  67. package/web/assets/{arc-Dk_KI36B.js → arc-B0u5yBJA.js} +1 -1
  68. package/web/assets/{architectureDiagram-ZJ3FMSHR-NR_KhRCf.js → architectureDiagram-ZJ3FMSHR-DfxOAo8P.js} +1 -1
  69. package/web/assets/{ask-tray-TzeTcO6-.js → ask-tray-SscJecDY.js} +1 -1
  70. package/web/assets/{blockDiagram-677ZJIJ3-CkLkwx9C.js → blockDiagram-677ZJIJ3-Ci3Usxh6.js} +1 -1
  71. package/web/assets/{c4Diagram-LMCZKHZV-CFCUH8e_.js → c4Diagram-LMCZKHZV-BqgwIVgh.js} +1 -1
  72. package/web/assets/channel-X7WHyxxT.js +1 -0
  73. package/web/assets/{chunk-2Q5K7J3B-B_BPLWsW.js → chunk-2Q5K7J3B-CfX69l2U.js} +1 -1
  74. package/web/assets/{chunk-32BRIVSS-B1VB1hOy.js → chunk-32BRIVSS-CuFlzZkl.js} +1 -1
  75. package/web/assets/{chunk-5VM5RSS4-KFcYr8Co.js → chunk-5VM5RSS4-DidCMlJa.js} +1 -1
  76. package/web/assets/{chunk-EX3LRPZG-CpBX3kuK.js → chunk-EX3LRPZG-B5CDm1dS.js} +1 -1
  77. package/web/assets/{chunk-JWPE2WC7-BPSl40wG.js → chunk-JWPE2WC7-D5fGV_sH.js} +1 -1
  78. package/web/assets/{chunk-MOJQB5TN-D2RcYmoU.js → chunk-MOJQB5TN-ayhH5Q-H.js} +1 -1
  79. package/web/assets/{chunk-RYQCIY6F-m7Gyn-t3.js → chunk-RYQCIY6F-C4BfOOZn.js} +1 -1
  80. package/web/assets/{chunk-V7JOEXUC-TDvdopxS.js → chunk-V7JOEXUC-DpUEIM9V.js} +1 -1
  81. package/web/assets/{chunk-VR4S4FIN-DZTsG51a.js → chunk-VR4S4FIN-DoQuFFRh.js} +1 -1
  82. package/web/assets/{chunk-XXDRQBXY-Dp0--RE2.js → chunk-XXDRQBXY-CI9eiLwn.js} +1 -1
  83. package/web/assets/classDiagram-OUVF2IWQ-BjsLQ3dz.js +1 -0
  84. package/web/assets/classDiagram-v2-EOCWNBFH-BjsLQ3dz.js +1 -0
  85. package/web/assets/client-7OWw5kW2.js +6 -0
  86. package/web/assets/client-DYj_td4Q.js +17 -0
  87. package/web/assets/{client-j2kQmxsO.js → client-Dk-NBvJR.js} +4 -4
  88. package/web/assets/command-zkzH4hVz.js +38 -0
  89. package/web/assets/{copy-context-C0Uqsp-3.js → copy-context-D5oiWrYC.js} +1 -1
  90. package/web/assets/{cose-bilkent-JH36ORCC-CPu3PI8h.js → cose-bilkent-JH36ORCC-BHRk1mO3.js} +1 -1
  91. package/web/assets/{cynefin-VYW2F7L2-DWxftYAm.js → cynefin-VYW2F7L2-C1B09TRE.js} +1 -1
  92. package/web/assets/{cynefinDiagram-TSTJHNR4-DsmN2V-p.js → cynefinDiagram-TSTJHNR4-B27qXQbv.js} +1 -1
  93. package/web/assets/{dagre-VKFMJZFB-COEbApyY.js → dagre-VKFMJZFB-B1qEi32g.js} +1 -1
  94. package/web/assets/{diagram-FQU43EPY-0O9WGWKB.js → diagram-FQU43EPY-BMkkgADD.js} +1 -1
  95. package/web/assets/{diagram-G47NLZAW-CutEdMVh.js → diagram-G47NLZAW-Cb8_g1A5.js} +1 -1
  96. package/web/assets/{diagram-NH7WQ7WH-C9KZc3B2.js → diagram-NH7WQ7WH-B51wuc5m.js} +1 -1
  97. package/web/assets/{diagram-OA4YK3LP-D8kkPXbY.js → diagram-OA4YK3LP-xnsecbbI.js} +1 -1
  98. package/web/assets/{diagram-WEI45ONY-_YqiTaGP.js → diagram-WEI45ONY-DSZdyaAV.js} +1 -1
  99. package/web/assets/{document-Dgt5eZFj.js → document-Dc2dftTW.js} +3 -3
  100. package/web/assets/{ebnfDiagram-CCIWWBDH-JO-zQh6g.js → ebnfDiagram-CCIWWBDH-aTHmcUGy.js} +1 -1
  101. package/web/assets/{entities-C3wz7FCx.js → entities-CCUtpskY.js} +1 -1
  102. package/web/assets/{erDiagram-Q63AITRT-9UjQwJ4P.js → erDiagram-Q63AITRT-CUUPBcvo.js} +1 -1
  103. package/web/assets/{file-viewer-DZJ8nYnl.js → file-viewer-DJ3rsmef.js} +1 -1
  104. package/web/assets/{flowDiagram-23GEKE2U-Ca08tnYM.js → flowDiagram-23GEKE2U-aTQXx2Mh.js} +1 -1
  105. package/web/assets/{folder-image-BfGgxo7i.js → folder-image-Bnv43tzP.js} +1 -1
  106. package/web/assets/{folder-landing-qUKBCnG-.js → folder-landing-DCnzYXXY.js} +1 -1
  107. package/web/assets/{folder-list-BfFjv7rR.js → folder-list-DvVkVjiR.js} +1 -1
  108. package/web/assets/{folder-metadata-DCw8xzHk.js → folder-metadata-C9hMMzIH.js} +1 -1
  109. package/web/assets/{folder-mobile-bar-Ubz4babf.js → folder-mobile-bar-VoTL1GS0.js} +1 -1
  110. package/web/assets/folder-plugin-view-Cvd804ZU.js +6 -0
  111. package/web/assets/{ganttDiagram-NO4QXBWP-KnHzX5W3.js → ganttDiagram-NO4QXBWP-DiwdUp7u.js} +1 -1
  112. package/web/assets/{gitGraphDiagram-IHSO6WYX-Cm8woNtK.js → gitGraphDiagram-IHSO6WYX-BL2gG_IS.js} +1 -1
  113. package/web/assets/{index-CNyy98F8.js → index-CUTtMSyJ.js} +2 -2
  114. package/web/assets/index-CegvVQ1C.js +1 -0
  115. package/web/assets/index-Dwhf8F_U.css +1 -0
  116. package/web/assets/{infoDiagram-FWYZ7A6U-yyqmUtIb.js → infoDiagram-FWYZ7A6U-B3n0zZpt.js} +1 -1
  117. package/web/assets/{ishikawaDiagram-FXEZZL3T-BfvR_Zy4.js → ishikawaDiagram-FXEZZL3T-D7YytbEv.js} +1 -1
  118. package/web/assets/{journeyDiagram-5HDEW3XC-BKuw06HM.js → journeyDiagram-5HDEW3XC-BYwqEl3C.js} +1 -1
  119. package/web/assets/{kanban-definition-HUTT4EX6-n0010rmQ.js → kanban-definition-HUTT4EX6-BQHNJgXZ.js} +1 -1
  120. package/web/assets/{linear-C0d0lT8O.js → linear-BPiQCLNk.js} +1 -1
  121. package/web/assets/{mermaid.core-BDVbShyO.js → mermaid.core-CD-KvBjV.js} +4 -4
  122. package/web/assets/{metadata-KV0dpp4G.js → metadata-COJ7fZul.js} +1 -1
  123. package/web/assets/{mindmap-definition-LN4V7U3C--_0ks3GF.js → mindmap-definition-LN4V7U3C-C1L_gyoj.js} +1 -1
  124. package/web/assets/{pegDiagram-2B236MQR-CBoNIpKR.js → pegDiagram-2B236MQR-BDAfItqS.js} +1 -1
  125. package/web/assets/{pieDiagram-ENE6RG2P-MA5tkYh5.js → pieDiagram-ENE6RG2P-CkZYH3Vz.js} +1 -1
  126. package/web/assets/{popover-DBD7FF-W.js → popover-0oA2lLUV.js} +2 -2
  127. package/web/assets/{project-search-dialog-DM1IkSZu.js → project-search-dialog-B2oqMT6h.js} +1 -1
  128. package/web/assets/{quadrantDiagram-ABIIQ3AL-CWT1ryqF.js → quadrantDiagram-ABIIQ3AL-Dy8Al3YZ.js} +1 -1
  129. package/web/assets/{railroadDiagram-RFXS5EU6-ZiLB5FGi.js → railroadDiagram-RFXS5EU6-Dd9_yo7B.js} +1 -1
  130. package/web/assets/{requirementDiagram-TGXJPOKE-CQq9uneq.js → requirementDiagram-TGXJPOKE-DPhIUsbs.js} +1 -1
  131. package/web/assets/{sankeyDiagram-HTMAVEWB-qDG-0JqI.js → sankeyDiagram-HTMAVEWB-B2QkYtub.js} +1 -1
  132. package/web/assets/{search-DNrQA5fF.js → search-C1J0-xiB.js} +1 -1
  133. package/web/assets/select-CtXC_PLC.js +30 -0
  134. package/web/assets/{selection-popup-DmtTeRs2.js → selection-popup-qhlUFzGq.js} +1 -1
  135. package/web/assets/{sequenceDiagram-DBY2YBRQ-CiApurLO.js → sequenceDiagram-DBY2YBRQ-Bx7fcZDx.js} +1 -1
  136. package/web/assets/{sizeCapture-X5ZJPWSS-DZSR5QXm.js → sizeCapture-X5ZJPWSS-CIXIhl_A.js} +1 -1
  137. package/web/assets/{stateDiagram-2N3HPSRC-CjJJR9_z.js → stateDiagram-2N3HPSRC-DCrdAflB.js} +1 -1
  138. package/web/assets/stateDiagram-v2-6OUMAXLB-0KCFLYeQ.js +1 -0
  139. package/web/assets/{swimlanes-5IMT3BWC-CKmo1sg6.js → swimlanes-5IMT3BWC-DA2vc7iZ.js} +2 -2
  140. package/web/assets/swimlanesDiagram-G3AALYLV-CAw3AV84.js +8 -0
  141. package/web/assets/{timeline-definition-FHXFAJF6-D-NMwS3y.js → timeline-definition-FHXFAJF6-lKfJiV9V.js} +1 -1
  142. package/web/assets/{vennDiagram-L72KCM5P-CfL6LBWw.js → vennDiagram-L72KCM5P-DO1q2KzG.js} +1 -1
  143. package/web/assets/{view-context-gyDl_p2V.js → view-context-DXKM3-l1.js} +4 -4
  144. package/web/assets/{viewer-capabilities-Bpp8Uvcg.js → viewer-capabilities-Cig_xM9u.js} +1 -1
  145. package/web/assets/{wardleyDiagram-EHGQE667-DgldMxaj.js → wardleyDiagram-EHGQE667-Dal9LkuZ.js} +1 -1
  146. package/web/assets/{xychartDiagram-FW5EYKEG-CIoAIFX8.js → xychartDiagram-FW5EYKEG-B3dPLPII.js} +1 -1
  147. package/web/index.html +2 -2
  148. package/kernel/packages/api/test/partial-loading.test.ts +0 -43
  149. package/kernel/packages/folder-plugin/package.json +0 -50
  150. package/kernel/packages/folder-plugin/src/index.ts +0 -15
  151. package/kernel/packages/folder-plugin/tsconfig.json +0 -17
  152. package/kernel/plugins/calendar/SKILL.md +0 -173
  153. package/kernel/plugins/calendar/eslint.config.js +0 -10
  154. package/kernel/plugins/calendar/package.json +0 -73
  155. package/kernel/plugins/calendar/schema.json +0 -137
  156. package/kernel/plugins/calendar/src/calendar.tsx +0 -328
  157. package/kernel/plugins/calendar/src/client.tsx +0 -139
  158. package/kernel/plugins/calendar/src/context.ts +0 -48
  159. package/kernel/plugins/calendar/src/copy-button.tsx +0 -66
  160. package/kernel/plugins/calendar/src/event-dialog.tsx +0 -270
  161. package/kernel/plugins/calendar/src/index.ts +0 -4
  162. package/kernel/plugins/calendar/src/manifest.ts +0 -7
  163. package/kernel/plugins/calendar/src/parse.ts +0 -610
  164. package/kernel/plugins/calendar/src/server.ts +0 -160
  165. package/kernel/plugins/calendar/src/utils.ts +0 -80
  166. package/kernel/plugins/calendar/src/views.tsx +0 -418
  167. package/kernel/plugins/calendar/test/context.test.mjs +0 -60
  168. package/kernel/plugins/calendar/test/package.test.mjs +0 -43
  169. package/kernel/plugins/calendar/test/parse.test.mjs +0 -392
  170. package/kernel/plugins/calendar/test/server.test.mjs +0 -126
  171. package/kernel/plugins/calendar/tsconfig.json +0 -17
  172. package/kernel/plugins/mail/SKILL.md +0 -97
  173. package/kernel/plugins/mail/eslint.config.js +0 -10
  174. package/kernel/plugins/mail/src/api.ts +0 -359
  175. package/kernel/plugins/mail/src/bun-ffi.d.ts +0 -14
  176. package/kernel/plugins/mail/src/client.tsx +0 -14
  177. package/kernel/plugins/mail/src/flock.ts +0 -137
  178. package/kernel/plugins/mail/src/folders.ts +0 -37
  179. package/kernel/plugins/mail/src/history.ts +0 -485
  180. package/kernel/plugins/mail/src/index.ts +0 -3
  181. package/kernel/plugins/mail/src/mail-card.tsx +0 -123
  182. package/kernel/plugins/mail/src/mail-conversation.tsx +0 -153
  183. package/kernel/plugins/mail/src/mail-detail-page.tsx +0 -467
  184. package/kernel/plugins/mail/src/mail-hooks.ts +0 -252
  185. package/kernel/plugins/mail/src/mail-page.tsx +0 -109
  186. package/kernel/plugins/mail/src/mail-request.ts +0 -78
  187. package/kernel/plugins/mail/src/mail-send-box.tsx +0 -175
  188. package/kernel/plugins/mail/src/mailos.ts +0 -723
  189. package/kernel/plugins/mail/src/manifest.ts +0 -7
  190. package/kernel/plugins/mail/src/node.ts +0 -171
  191. package/kernel/plugins/mail/src/parse.ts +0 -139
  192. package/kernel/plugins/mail/src/server.ts +0 -125
  193. package/kernel/plugins/mail/src/types.ts +0 -10
  194. package/kernel/plugins/mail/src/viewer.tsx +0 -147
  195. package/kernel/plugins/mail/test/flock.test.mjs +0 -156
  196. package/kernel/plugins/mail/test/folders.test.mjs +0 -138
  197. package/kernel/plugins/mail/test/history.test.mjs +0 -460
  198. package/kernel/plugins/mail/test/mailos.test.mjs +0 -752
  199. package/kernel/plugins/mail/test/node.test.mjs +0 -207
  200. package/kernel/plugins/mail/test/package.test.mjs +0 -54
  201. package/kernel/plugins/mail/test/parse.test.mjs +0 -32
  202. package/kernel/plugins/mail/test/server.test.mjs +0 -116
  203. package/kernel/plugins/mail/test/viewer.test.mjs +0 -192
  204. package/kernel/plugins/mail/tsconfig.json +0 -17
  205. package/kernel/plugins/projects/SKILL.md +0 -73
  206. package/kernel/plugins/projects/eslint.config.js +0 -10
  207. package/kernel/plugins/projects/package.json +0 -70
  208. package/kernel/plugins/projects/src/client.tsx +0 -17
  209. package/kernel/plugins/projects/src/data/model.ts +0 -129
  210. package/kernel/plugins/projects/src/index.ts +0 -17
  211. package/kernel/plugins/projects/src/manifest.ts +0 -7
  212. package/kernel/plugins/projects/src/project-context.ts +0 -41
  213. package/kernel/plugins/projects/src/server.ts +0 -276
  214. package/kernel/plugins/projects/src/ui/project-card.tsx +0 -61
  215. package/kernel/plugins/projects/src/ui/projects-page.tsx +0 -102
  216. package/kernel/plugins/projects/test/package.test.mjs +0 -51
  217. package/kernel/plugins/tasks/SKILL.md +0 -341
  218. package/kernel/plugins/tasks/eslint.config.js +0 -10
  219. package/kernel/plugins/tasks/package.json +0 -74
  220. package/kernel/plugins/tasks/schema.json +0 -939
  221. package/kernel/plugins/tasks/src/api.ts +0 -212
  222. package/kernel/plugins/tasks/src/client.tsx +0 -23
  223. package/kernel/plugins/tasks/src/data/parse.ts +0 -544
  224. package/kernel/plugins/tasks/src/data/task-id.ts +0 -56
  225. package/kernel/plugins/tasks/src/data/todo-model.ts +0 -795
  226. package/kernel/plugins/tasks/src/index.ts +0 -34
  227. package/kernel/plugins/tasks/src/manifest.ts +0 -7
  228. package/kernel/plugins/tasks/src/pages/task-detail.tsx +0 -48
  229. package/kernel/plugins/tasks/src/pages/task-list.tsx +0 -88
  230. package/kernel/plugins/tasks/src/server.ts +0 -447
  231. package/kernel/plugins/tasks/src/styles.css +0 -9
  232. package/kernel/plugins/tasks/src/ui/compact-todo-list.tsx +0 -372
  233. package/kernel/plugins/tasks/src/ui/error-details.tsx +0 -78
  234. package/kernel/plugins/tasks/src/ui/task-page.tsx +0 -392
  235. package/kernel/plugins/tasks/src/ui/todo-agent-filter.tsx +0 -36
  236. package/kernel/plugins/tasks/src/ui/todo-filter-bar.tsx +0 -66
  237. package/kernel/plugins/tasks/src/ui/todo-item.tsx +0 -494
  238. package/kernel/plugins/tasks/src/ui/todo-status-filter.tsx +0 -60
  239. package/kernel/plugins/tasks/src/ui/todo-tag-filter.tsx +0 -48
  240. package/kernel/plugins/tasks/src/ui/todo-warnings.tsx +0 -106
  241. package/kernel/plugins/tasks/test/api.test.mjs +0 -99
  242. package/kernel/plugins/tasks/test/folder-sources.test.mjs +0 -46
  243. package/kernel/plugins/tasks/test/package.test.mjs +0 -54
  244. package/kernel/plugins/tasks/test/parse.test.mjs +0 -225
  245. package/kernel/plugins/tasks/test/server.test.mjs +0 -386
  246. package/kernel/plugins/tasks/tsconfig.json +0 -17
  247. package/web/assets/app-DoGr2Zi7.js +0 -2
  248. package/web/assets/channel-uMEjV8qc.js +0 -1
  249. package/web/assets/classDiagram-OUVF2IWQ-fbZocVor.js +0 -1
  250. package/web/assets/classDiagram-v2-EOCWNBFH-fbZocVor.js +0 -1
  251. package/web/assets/client-C6Nxdfif.js +0 -2
  252. package/web/assets/client-CXYfBko5.js +0 -17
  253. package/web/assets/client-CoKtOq-X.js +0 -7
  254. package/web/assets/client-DyRQz5RU.js +0 -53
  255. package/web/assets/client-OY6HUzuD.js +0 -98
  256. package/web/assets/client-YOYWZIFi.js +0 -34
  257. package/web/assets/command-BugZBGXE.js +0 -38
  258. package/web/assets/folder-plugin-view-DsWR1zmQ.js +0 -6
  259. package/web/assets/index-BaRnbYRw.js +0 -1
  260. package/web/assets/index-BexmLxIP.css +0 -1
  261. package/web/assets/index-DDirZMgx.js +0 -1
  262. package/web/assets/note-card-DBfX6gW1.js +0 -6
  263. package/web/assets/select-D8cqNgcp.js +0 -30
  264. package/web/assets/stateDiagram-v2-6OUMAXLB-BpeGSCu9.js +0 -1
  265. package/web/assets/swimlanesDiagram-G3AALYLV-D9RjLKb9.js +0 -8
  266. package/web/assets/use-folder-filter-LQyXns5Q.js +0 -1
  267. /package/kernel/packages/{folder-plugin → plugins}/eslint.config.js +0 -0
  268. /package/kernel/packages/{folder-plugin → plugins}/src/client.ts +0 -0
  269. /package/kernel/packages/{folder-plugin → plugins}/src/endpoint.ts +0 -0
  270. /package/kernel/packages/{folder-plugin → plugins}/src/server.ts +0 -0
  271. /package/kernel/packages/{folder-plugin → plugins}/src/subpath.ts +0 -0
  272. /package/kernel/packages/{folder-plugin → plugins}/src/types.ts +0 -0
  273. /package/kernel/packages/{folder-plugin → plugins}/src/validation.ts +0 -0
  274. /package/kernel/packages/{folder-plugin → plugins}/test/contract.test.mjs +0 -0
@@ -1,752 +0,0 @@
1
- import { beforeEach, describe, expect, test } from "bun:test"
2
-
3
- import {
4
- conversationLimits,
5
- createMailosService,
6
- MailosCommandError,
7
- MailSendFailure,
8
- resetConversationCache,
9
- runMailos,
10
- validateSendContract,
11
- } from "../src/mailos.ts"
12
-
13
- const REV = "a".repeat(64)
14
- const BIN = "/tmp/mailos-test-bin"
15
- const DRAFT_DIR = "/tmp/mailos-test-drafts"
16
-
17
- function readMarkdown(id) {
18
- return [
19
- `# Subject ${id}`,
20
- "",
21
- "**From:** sender@example.com ",
22
- "**To:** to@example.com ",
23
- "**Date:** September 21, 2026 3:04 PM ",
24
- "**ID:** 42 ",
25
- "",
26
- "---",
27
- "",
28
- `Body of ${id}`,
29
- "",
30
- ].join("\n")
31
- }
32
-
33
- const defaultHandler = () => ({
34
- stdout: readMarkdown("x"),
35
- stderr: "",
36
- })
37
-
38
- function recordingRunner(handlers = [], fallback = defaultHandler) {
39
- const calls = []
40
- let index = 0
41
- const runner = async (file, args, options) => {
42
- const call = {
43
- args,
44
- cwd: options.cwd,
45
- file,
46
- signal: options.signal,
47
- timeout: options.timeout,
48
- }
49
- calls.push(call)
50
- const handler = handlers[index] ?? fallback
51
- index += 1
52
- return handler(call)
53
- }
54
- return { calls, runner }
55
- }
56
-
57
- function service(handlers = [], options = {}) {
58
- const { calls, runner } = recordingRunner(handlers)
59
- const svc = createMailosService({
60
- execFile: runner,
61
- mailosBin: BIN,
62
- globalDraftDir: DRAFT_DIR,
63
- ...options,
64
- })
65
- return { calls, svc }
66
- }
67
-
68
- function notFoundError(id) {
69
- return new MailosCommandError("not-found", `Message ${id} not found`)
70
- }
71
-
72
- beforeEach(() => {
73
- resetConversationCache()
74
- })
75
-
76
- describe("runMailos lifecycle", () => {
77
- test("maps killed children to timeout with the exit code", async () => {
78
- const runner = async () => {
79
- throw Object.assign(new Error("boom"), { killed: true, code: 1 })
80
- }
81
- await expect(
82
- runMailos(runner, BIN, ["x"], { cwd: "/f", timeoutMs: 100 }),
83
- ).rejects.toMatchObject({ code: "timeout", timedOut: true, exitCode: 1 })
84
- })
85
-
86
- test("maps AbortError to aborted", async () => {
87
- const runner = async () => {
88
- const error = new Error("aborted")
89
- error.name = "AbortError"
90
- throw error
91
- }
92
- await expect(
93
- runMailos(runner, BIN, ["x"], { cwd: "/f", timeoutMs: 100 }),
94
- ).rejects.toMatchObject({ code: "aborted", aborted: true })
95
- })
96
-
97
- test("redacts command output from error messages", async () => {
98
- const runner = async () => {
99
- throw new Error("stderr contained secret-token-xyz and body content")
100
- }
101
- await expect(
102
- runMailos(runner, BIN, ["x"], { cwd: "/f", timeoutMs: 100 }),
103
- ).rejects.toMatchObject({
104
- code: "failed",
105
- message: "MailOS command failed to complete",
106
- })
107
- })
108
-
109
- test("passes the abort signal to the child", async () => {
110
- const seen = {}
111
- const runner = async (_file, _args, options) => {
112
- seen.signal = options.signal
113
- return { stdout: "", stderr: "" }
114
- }
115
- const controller = new AbortController()
116
- await runMailos(runner, BIN, ["x"], {
117
- cwd: "/f",
118
- signal: controller.signal,
119
- timeoutMs: 100,
120
- })
121
- expect(seen.signal).toBe(controller.signal)
122
- })
123
-
124
- test("rejects immediately when the signal is already aborted", async () => {
125
- const controller = new AbortController()
126
- controller.abort()
127
- const runner = async () => ({ stdout: "", stderr: "" })
128
- await expect(
129
- runMailos(runner, BIN, ["x"], {
130
- cwd: "/f",
131
- signal: controller.signal,
132
- timeoutMs: 100,
133
- }),
134
- ).rejects.toMatchObject({ code: "aborted" })
135
- })
136
- })
137
-
138
- describe("identity", () => {
139
- test("parses Effective From and From Name, never errors the row", async () => {
140
- const { svc, calls } = service([
141
- () => ({
142
- stdout: "From Name: Jane Doe\nEffective From: jane@example.com\n",
143
- stderr: "",
144
- }),
145
- ])
146
- const identity = await svc.identity("/f")
147
- expect(identity).toEqual({
148
- from: "jane@example.com",
149
- name: "Jane Doe",
150
- ok: true,
151
- })
152
- expect(calls[0].args).toEqual(["whoami"])
153
- expect(calls[0].cwd).toBe("/f")
154
- })
155
-
156
- test('normalizes "(not set)" placeholders to null instead of a fake identity', async () => {
157
- const { svc } = service([
158
- () => ({
159
- stdout: "From Name: (not set)\nEffective From: (not set)\n",
160
- stderr: "",
161
- }),
162
- ])
163
- expect(await svc.identity("/f")).toEqual({
164
- from: null,
165
- name: null,
166
- ok: false,
167
- })
168
- })
169
-
170
- test("treats a blank identity as unset", async () => {
171
- const { svc } = service([
172
- () => ({ stdout: "From Name:\nEffective From:\n", stderr: "" }),
173
- ])
174
- expect(await svc.identity("/f")).toEqual({
175
- from: null,
176
- name: null,
177
- ok: false,
178
- })
179
- })
180
-
181
- test("CLI failure yields a graceful null identity", async () => {
182
- const { svc } = service([
183
- () => {
184
- throw new Error("boom")
185
- },
186
- ])
187
- expect(await svc.identity("/f")).toEqual({
188
- from: null,
189
- name: null,
190
- ok: false,
191
- })
192
- })
193
- })
194
-
195
- describe("signature", () => {
196
- test("resolves the folder signature with exact args", async () => {
197
- const { svc, calls } = service([
198
- () => ({
199
- stdout: "**Example User**\nDirector, Example Company\n",
200
- stderr: "",
201
- }),
202
- ])
203
- expect(await svc.signature("/f/clients")).toBe(
204
- "**Example User**\nDirector, Example Company",
205
- )
206
- expect(calls[0].args).toEqual(["sig", "/f/clients"])
207
- expect(calls[0].cwd).toBe("/f/clients")
208
- })
209
-
210
- test("returns null when the folder has no signature configured", async () => {
211
- const { svc } = service([
212
- () => ({ stdout: "(no signature configured)\n", stderr: "" }),
213
- ])
214
- expect(await svc.signature("/f")).toBeNull()
215
- })
216
-
217
- test("returns null for an empty signature", async () => {
218
- const { svc } = service([
219
- () => ({ stdout: "(signature is empty)\n", stderr: "" }),
220
- ])
221
- expect(await svc.signature("/f")).toBeNull()
222
- })
223
-
224
- test("blank output degrades to null", async () => {
225
- const { svc } = service([() => ({ stdout: "", stderr: "" })])
226
- expect(await svc.signature("/f")).toBeNull()
227
- })
228
-
229
- test("CLI failure yields a graceful null signature", async () => {
230
- const { svc } = service([
231
- () => {
232
- throw new Error("boom")
233
- },
234
- ])
235
- expect(await svc.signature("/f")).toBeNull()
236
- })
237
- })
238
-
239
- describe("conversation", () => {
240
- test("reads references with exact args and returns ok items in order", async () => {
241
- const { svc, calls } = service([
242
- () => ({ stdout: readMarkdown("r1"), stderr: "" }),
243
- () => ({ stdout: readMarkdown("r2"), stderr: "" }),
244
- ])
245
- const result = await svc.conversation("/f", ["r1", "r2"])
246
- expect(result.outcome).toBe("ok")
247
- expect(result.items.map((item) => item.id)).toEqual(["r1", "r2"])
248
- expect(result.items[0]).toMatchObject({
249
- subject: "Subject r1",
250
- from: "sender@example.com",
251
- to: "to@example.com",
252
- date: "September 21, 2026 3:04 PM",
253
- })
254
- expect(result.items[0].body).toBe("Body of r1")
255
- expect(calls.map((call) => call.args)).toEqual([
256
- ["read", "r1", "--markdown"],
257
- ["read", "r2", "--markdown"],
258
- ])
259
- })
260
-
261
- test("empty references do not invoke the CLI", async () => {
262
- const { svc, calls } = service([])
263
- const result = await svc.conversation("/f", [])
264
- expect(result).toEqual({ items: [], missing: [], outcome: "empty" })
265
- expect(calls.length).toBe(0)
266
- })
267
-
268
- test("bounds references to the configured maximum", async () => {
269
- const ids = Array.from({ length: 20 }, (_, index) => `r${index}`)
270
- const { svc, calls } = service([])
271
- const result = await svc.conversation("/f", ids)
272
- expect(result.items.length).toBe(conversationLimits.maxReferences)
273
- expect(calls.length).toBe(conversationLimits.maxReferences)
274
- })
275
-
276
- test("deduplicates duplicate references within one request", async () => {
277
- const { svc, calls } = service([])
278
- const result = await svc.conversation("/f", ["r1", "r1", "r1"])
279
- expect(result.items.length).toBe(1)
280
- expect(calls.length).toBe(1)
281
- })
282
-
283
- test("classifies a partially resolved thread", async () => {
284
- const { svc } = service([
285
- () => ({ stdout: readMarkdown("a"), stderr: "" }),
286
- () => {
287
- throw notFoundError("b")
288
- },
289
- () => ({ stdout: readMarkdown("c"), stderr: "" }),
290
- ])
291
- const result = await svc.conversation("/f", ["a", "b", "c"])
292
- expect(result.outcome).toBe("partial")
293
- expect(result.items.map((item) => item.id)).toEqual(["a", "c"])
294
- expect(result.missing).toEqual(["b"])
295
- })
296
-
297
- test("classifies an entirely unresolved thread as not-found", async () => {
298
- const { svc } = service([
299
- () => {
300
- throw notFoundError("a")
301
- },
302
- () => {
303
- throw notFoundError("b")
304
- },
305
- ])
306
- const result = await svc.conversation("/f", ["a", "b"])
307
- expect(result.outcome).toBe("not-found")
308
- expect(result.items).toEqual([])
309
- expect(result.missing).toEqual(["a", "b"])
310
- })
311
-
312
- test("classifies a timed-out read as timeout without retrying", async () => {
313
- const { svc, calls } = service([
314
- () => ({ stdout: readMarkdown("a"), stderr: "" }),
315
- () => {
316
- throw new MailosCommandError("timeout", "read timed out", {
317
- timedOut: true,
318
- })
319
- },
320
- ])
321
- const result = await svc.conversation("/f", ["a", "b"])
322
- expect(result.outcome).toBe("timeout")
323
- expect(result.items.map((item) => item.id)).toEqual(["a"])
324
- // One attempt per reference; never a retry loop.
325
- expect(calls.length).toBe(2)
326
- })
327
-
328
- test(
329
- "long-delay fixture: a read slower than the old 8s cap still succeeds on real timers",
330
- { timeout: 60_000 },
331
- async () => {
332
- const startedAt = performance.now()
333
- const slowDelayMs = 9_500
334
- const { svc } = service([
335
- () => {
336
- return Bun.sleep(slowDelayMs).then(() => ({
337
- stdout: readMarkdown("slow-real"),
338
- stderr: "",
339
- }))
340
- },
341
- ])
342
- const result = await svc.conversation("/f", ["slow-real"])
343
- const elapsed = performance.now() - startedAt
344
- // The per-read cap (60s) sits above the measured 22-26s success envelope
345
- // and above this 9.5s fixture; the old 8s cap would have killed it.
346
- expect(result.outcome).toBe("ok")
347
- expect(result.items.map((item) => item.id)).toEqual(["slow-real"])
348
- expect(elapsed).toBeGreaterThanOrEqual(slowDelayMs)
349
- },
350
- )
351
-
352
- test("keeps slow-but-successful reads above the old 8s cap (20s per read)", async () => {
353
- const realNow = Date.now
354
- let now = 0
355
- Date.now = () => now
356
- try {
357
- const { svc } = service([
358
- () => {
359
- now += 20_500 // a slow-but-successful MailOS read (~20.5s)
360
- return { stdout: readMarkdown("slow-a"), stderr: "" }
361
- },
362
- () => {
363
- now += 20_500
364
- return { stdout: readMarkdown("slow-b"), stderr: "" }
365
- },
366
- ])
367
- const result = await svc.conversation("/f", ["slow-a", "slow-b"])
368
- // Two ~20.5s reads fit inside the 75s budget with 60s per-read caps.
369
- expect(result.outcome).toBe("ok")
370
- expect(result.items.map((item) => item.id)).toEqual(["slow-a", "slow-b"])
371
- } finally {
372
- Date.now = realNow
373
- }
374
- })
375
-
376
- test("stops once the total time budget is exhausted", async () => {
377
- const realNow = Date.now
378
- let now = 0
379
- Date.now = () => now
380
- try {
381
- const { svc, calls } = service([
382
- () => {
383
- now += 40_000 // within the 60s per-read cap
384
- return { stdout: readMarkdown("a"), stderr: "" }
385
- },
386
- () => {
387
- now += 40_000 // 80s total now exceeds the 75s budget
388
- return { stdout: readMarkdown("b"), stderr: "" }
389
- },
390
- () => ({ stdout: readMarkdown("c"), stderr: "" }),
391
- ])
392
- const result = await svc.conversation("/f", ["a", "b", "c"])
393
- expect(result.outcome).toBe("timeout")
394
- expect(result.items.length).toBeGreaterThanOrEqual(1)
395
- // No blind retry loop: each reference gets at most one attempt.
396
- expect(calls.length).toBe(3)
397
- } finally {
398
- Date.now = realNow
399
- }
400
- })
401
-
402
- test("per-read cap and budget sit above the measured 22-26s success envelope", () => {
403
- expect(conversationLimits.perReadTimeoutMs).toBeGreaterThanOrEqual(60_000)
404
- expect(conversationLimits.totalBudgetMs).toBeGreaterThanOrEqual(75_000)
405
- expect(conversationLimits.perReadTimeoutMs).toBeLessThan(
406
- conversationLimits.totalBudgetMs,
407
- )
408
- })
409
-
410
- test("deduplicates in-flight reads across concurrent callers", async () => {
411
- const { svc, calls } = service([])
412
- const [left, right] = await Promise.all([
413
- svc.conversation("/f", ["shared", "left"]),
414
- svc.conversation("/f", ["shared", "right"]),
415
- ])
416
- expect(left.outcome).toBe("ok")
417
- expect(right.outcome).toBe("ok")
418
- const sharedCalls = calls.filter((call) => call.args[1] === "shared")
419
- expect(sharedCalls.length).toBe(1)
420
- expect(calls.length).toBe(3)
421
- })
422
-
423
- test("serves repeated requests from the bounded success cache per scope", async () => {
424
- const { svc, calls } = service([])
425
- await svc.conversation("/f", ["hot"])
426
- const afterFirst = calls.length
427
- const cached = await svc.conversation("/f", ["hot"])
428
- expect(cached.outcome).toBe("ok")
429
- expect(calls.length).toBe(afterFirst)
430
-
431
- // A different folder scope must not reuse the cached read.
432
- await svc.conversation("/g", ["hot"])
433
- expect(calls.length).toBe(afterFirst + 1)
434
- })
435
-
436
- test("evicts oldest successes when the cache is full", async () => {
437
- const { svc, calls } = service([])
438
- const batches = Array.from({ length: 5 }, (_, batch) =>
439
- Array.from(
440
- { length: conversationLimits.maxReferences },
441
- (_, index) => `k${batch}-${index}`,
442
- ),
443
- )
444
- for (const batch of batches) await svc.conversation("/f", batch)
445
- const afterBulk = calls.length
446
- expect(afterBulk).toBeGreaterThan(conversationLimits.successCacheSize)
447
- // The oldest success was evicted: it must hit the CLI again.
448
- await svc.conversation("/f", ["k0-0"])
449
- expect(calls.length).toBe(afterBulk + 1)
450
- // A recently cached id is still served from memory.
451
- await svc.conversation("/f", ["k4-11"])
452
- expect(calls.length).toBe(afterBulk + 1)
453
- })
454
-
455
- test("cancels the child on caller disconnect without touching shared consumers", async () => {
456
- const { svc } = service([])
457
- const cancelled = new AbortController()
458
- cancelled.abort()
459
- await expect(
460
- svc.conversation("/f", ["x"], cancelled.signal),
461
- ).rejects.toMatchObject({
462
- code: "aborted",
463
- })
464
- })
465
-
466
- test("one canceled subscriber never kills another consumer of the same read", async () => {
467
- // A gated runner keeps the shared read in flight until the test decides:
468
- // with an instantly-resolving fake the read would be done (and cached)
469
- // before the abort, so this must deliberately hold the child open.
470
- const calls = []
471
- let gateResolve = null
472
- const gate = new Promise((resolveGate) => {
473
- gateResolve = resolveGate
474
- })
475
- const runner = async (_file, args) => {
476
- calls.push(args)
477
- if (args[1] === "shared") await gate
478
- return { stdout: readMarkdown(args[1]), stderr: "" }
479
- }
480
- const svc = createMailosService({
481
- execFile: runner,
482
- mailosBin: BIN,
483
- globalDraftDir: DRAFT_DIR,
484
- })
485
- const first = new AbortController()
486
- const second = new AbortController()
487
- const pending = svc.conversation("/f", ["shared"], first.signal)
488
- const sharedCall = new Promise((resolveCall) => {
489
- const started = () => resolveCall(undefined)
490
- const check = () => {
491
- if (calls.some((args) => args[1] === "shared")) started()
492
- }
493
- check()
494
- const timer = setInterval(check, 1)
495
- setTimeout(() => clearInterval(timer), 1000)
496
- })
497
- await sharedCall
498
- const other = svc.conversation("/f", ["shared", "other"], second.signal)
499
- // Let the second worker subscribe to the shared in-flight read.
500
- await Bun.sleep(5)
501
-
502
- // Aborting just the first consumer must not abort the shared child.
503
- first.abort()
504
- await expect(pending).rejects.toMatchObject({ code: "aborted" })
505
-
506
- // Release the shared read: the second subscriber still completes and
507
- // both consumers shared one read call.
508
- gateResolve()
509
- const result = await other
510
- expect(result.outcome).toBe("ok")
511
- // Two workers resolve the two ids independently; order is not part of
512
- // the guarantee, both messages must be present.
513
- expect(result.items.map((item) => item.id).sort()).toEqual([
514
- "other",
515
- "shared",
516
- ])
517
- const sharedCalls = calls.filter((args) => args[1] === "shared")
518
- expect(sharedCalls.length).toBe(1)
519
- })
520
-
521
- test("production path (with signal) still reuses cache and in-flight reads", async () => {
522
- const { svc, calls } = service([])
523
- const controller = new AbortController()
524
- const first = await svc.conversation("/f", ["hot"], controller.signal)
525
- expect(first.outcome).toBe("ok")
526
- const afterFirst = calls.length
527
-
528
- // A second production-style request with its own signal hits the cache.
529
- const second = await svc.conversation(
530
- "/f",
531
- ["hot"],
532
- new AbortController().signal,
533
- )
534
- expect(second.outcome).toBe("ok")
535
- expect(calls.length).toBe(afterFirst)
536
- })
537
- })
538
-
539
- describe("send preview and confirm", () => {
540
- function previewOutput() {
541
- return [
542
- "📋 PREVIEW - Would send draft: d_abc.md",
543
- `Revision: ${REV}`,
544
- "=== EMAIL PREVIEW ===",
545
- "To: to@example.com",
546
- "Subject: Hi",
547
- "",
548
- "--- Body ---",
549
- "Hello",
550
- "=== END PREVIEW ===",
551
- "Send now: mailos send draft d_abc --draft-revision ...",
552
- "",
553
- ].join("\n")
554
- }
555
-
556
- test("preview targets the authorized draft directory with exact args", async () => {
557
- const { svc, calls } = service([
558
- () => ({ stdout: previewOutput(), stderr: "" }),
559
- ])
560
- const preview = await svc.sendPreview("/f", "d_abc", {
561
- draftDir: DRAFT_DIR,
562
- })
563
- expect(preview.dryRun).toBe(true)
564
- expect(preview.revision).toBe(REV)
565
- expect(calls[0].args).toEqual([
566
- "send",
567
- "draft",
568
- "d_abc",
569
- "--draft-dir",
570
- DRAFT_DIR,
571
- "--preview",
572
- ])
573
- expect(calls[0].cwd).toBe("/f")
574
- })
575
-
576
- test("preview refuses to run without an explicit authorized draft directory", async () => {
577
- const { svc, calls } = service([])
578
- await expect(svc.sendPreview("/f", "d_abc")).rejects.toMatchObject({
579
- name: "MailSendFailure",
580
- reason: "invalid-draft",
581
- })
582
- expect(calls.length).toBe(0)
583
- })
584
-
585
- test("preview without a revision reports an unsendable draft", async () => {
586
- const { svc } = service([
587
- () => ({ stdout: "no revision here", stderr: "" }),
588
- ])
589
- await expect(
590
- svc.sendPreview("/f", "d_abc", { draftDir: DRAFT_DIR }),
591
- ).rejects.toMatchObject({
592
- name: "MailSendFailure",
593
- reason: "invalid-draft",
594
- })
595
- })
596
-
597
- test("confirm send uses the revision, the draft dir, and retains the draft", async () => {
598
- const { svc, calls } = service([
599
- () => ({
600
- stdout:
601
- "📤 Sending draft: d_abc.md ✅ Sent\n\n📊 Draft sending complete:\n ✅ Sent: 1\n 📁 Processed: 1 total drafts\n",
602
- stderr: "",
603
- }),
604
- ])
605
- const result = await svc.confirmSend("/f", "d_abc", REV, {
606
- draftDir: DRAFT_DIR,
607
- })
608
- expect(result.dryRun).toBe(false)
609
- expect(calls[0].args).toEqual([
610
- "send",
611
- "draft",
612
- "d_abc",
613
- "--draft-dir",
614
- DRAFT_DIR,
615
- "--draft-revision",
616
- REV,
617
- "--delete-after=false",
618
- ])
619
- })
620
-
621
- test("a malformed revision is rejected before any command runs", async () => {
622
- const { svc, calls } = service([])
623
- await expect(
624
- svc.confirmSend("/f", "d_abc", "not-a-hash", { draftDir: DRAFT_DIR }),
625
- ).rejects.toMatchObject({
626
- name: "MailSendFailure",
627
- reason: "revision-match",
628
- })
629
- expect(calls.length).toBe(0)
630
- })
631
-
632
- test("a send-time revision change is a revision conflict, not a generic failure", async () => {
633
- const { svc, calls } = service([
634
- () => ({
635
- stdout:
636
- "❌ Draft d_abc.md changed after preview\n Expected revision: aaa…\n Current revision: bbb…\n\n📊 Draft sending complete:\n ✅ Sent: 0\n ❌ Errors: 1\n 📁 Processed: 1 total drafts\n",
637
- stderr: "",
638
- }),
639
- ])
640
- await expect(
641
- svc.confirmSend("/f", "d_abc", REV, { draftDir: DRAFT_DIR }),
642
- ).rejects.toMatchObject({
643
- name: "MailSendFailure",
644
- reason: "revision-match",
645
- })
646
- expect(calls.length).toBe(1)
647
- })
648
-
649
- test("exit-0 output that reports skips or failures fails the contract", async () => {
650
- const { svc } = service([
651
- () => ({
652
- stdout:
653
- "⏭️ Skipped sent draft: d_abc.md\n\n📊 Draft sending complete:\n ✅ Sent: 0\n 📁 Processed: 1 total drafts\n",
654
- stderr: "",
655
- }),
656
- ])
657
- await expect(
658
- svc.confirmSend("/f", "d_abc", REV, { draftDir: DRAFT_DIR }),
659
- ).rejects.toMatchObject({
660
- name: "MailSendFailure",
661
- reason: "contract",
662
- })
663
- })
664
-
665
- test("cancellation aborts a send", async () => {
666
- const { svc } = service([])
667
- const controller = new AbortController()
668
- controller.abort()
669
- await expect(
670
- svc.confirmSend("/f", "d_abc", REV, {
671
- draftDir: DRAFT_DIR,
672
- signal: controller.signal,
673
- }),
674
- ).rejects.toMatchObject({
675
- code: "aborted",
676
- })
677
- })
678
- })
679
-
680
- describe("archive", () => {
681
- test("archives by canonical draft id in the global draft dir", async () => {
682
- const { svc, calls } = service([() => ({ stdout: "", stderr: "" })])
683
- await svc.archive("/f", "d_abc", { draftDir: DRAFT_DIR })
684
- expect(calls[0].args).toEqual(["drafts", "archive", "d_abc"])
685
- expect(calls[0].cwd).toBe("/f")
686
- })
687
-
688
- test("rejects a non-canonical archive target before any spawn", async () => {
689
- const { svc, calls } = service([])
690
- await expect(
691
- svc.archive("/f", "d_abc", { draftDir: "/tmp/elsewhere/drafts" }),
692
- ).rejects.toMatchObject({
693
- name: "MailSendFailure",
694
- reason: "unsupported-draft-dir",
695
- })
696
- // The CLI never ran: it would have mutated a same-named home draft.
697
- expect(calls.length).toBe(0)
698
- })
699
- })
700
-
701
- describe("validateSendContract", () => {
702
- test("accepts a confirmed single-draft send", () => {
703
- const output = [
704
- "📤 Sending draft: d_abc.md ✅ Sent",
705
- "",
706
- "📊 Draft sending complete:",
707
- " ✅ Sent: 1",
708
- " 📁 Processed: 1 total drafts",
709
- ].join("\n")
710
- expect(validateSendContract(output)).toEqual({ ok: true })
711
- })
712
-
713
- test("accepts a previewed run", () => {
714
- const output =
715
- "📊 Draft sending complete:\n ✅ Previewed: 1\n 📁 Processed: 1 total drafts\n"
716
- expect(validateSendContract(output)).toEqual({ ok: true })
717
- })
718
-
719
- test("rejects failure markers even with exit 0", () => {
720
- const output =
721
- "❌ Invalid draft d_abc: missing recipients\n📊 Draft sending complete:\n ✅ Sent: 0\n ❌ Errors: 1\n"
722
- expect(validateSendContract(output)).toMatchObject({ ok: false })
723
- })
724
-
725
- test("rejects skipped drafts", () => {
726
- expect(
727
- validateSendContract("⏭️ Skipped sent draft: d_abc.md\n ✅ Sent: 0\n"),
728
- ).toMatchObject({ ok: false })
729
- })
730
-
731
- test("rejects when no draft was found", () => {
732
- expect(
733
- validateSendContract("No draft files found to send\n ✅ Sent: 0\n"),
734
- ).toMatchObject({
735
- ok: false,
736
- })
737
- })
738
-
739
- test("rejects zero sent drafts", () => {
740
- expect(
741
- validateSendContract("📊 Draft sending complete:\n ✅ Sent: 0\n"),
742
- ).toMatchObject({
743
- ok: false,
744
- })
745
- })
746
- })
747
-
748
- test("MailSendFailure is an Error subclass", () => {
749
- const failure = new MailSendFailure("contract", "nope")
750
- expect(failure).toBeInstanceOf(Error)
751
- expect(failure.message).toBe("nope")
752
- })