@rubytech/create-maxy-code 0.1.477 → 0.1.479

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 (204) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1704-data-portal-standing-audit.md +673 -0
  3. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +1244 -0
  4. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +462 -0
  5. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +321 -0
  6. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +2177 -0
  7. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1704-data-portal-standing-audit-design.md +177 -0
  8. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +201 -0
  9. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +217 -0
  10. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +80 -0
  11. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1820-cpu-triage-admin-tools-design.md +97 -0
  12. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +163 -0
  13. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  14. package/payload/platform/lib/graph-style/dist/index.js +22 -0
  15. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  16. package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +6 -0
  17. package/payload/platform/lib/graph-style/src/index.ts +20 -0
  18. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +44 -0
  19. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -1
  20. package/payload/platform/lib/storage-broker/dist/audit.d.ts +56 -0
  21. package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
  22. package/payload/platform/lib/storage-broker/dist/audit.js +34 -0
  23. package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -1
  24. package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +70 -1
  25. package/payload/platform/lib/storage-broker/src/audit.ts +111 -0
  26. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  27. package/payload/platform/plugins/admin/PLUGIN.md +7 -1
  28. package/payload/platform/plugins/admin/mcp/dist/index.js +72 -0
  29. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  30. package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.d.ts +94 -0
  31. package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.d.ts.map +1 -0
  32. package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.js +229 -0
  33. package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.js.map +1 -0
  34. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +14 -2
  35. package/payload/platform/plugins/cloudflare/PLUGIN.md +26 -1
  36. package/payload/platform/plugins/cloudflare/bin/d1-http.mjs +80 -0
  37. package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +17 -2
  38. package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +357 -0
  39. package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +103 -0
  40. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +2 -2
  41. package/payload/platform/plugins/cloudflare/mcp/__tests__/d1-http.test.ts +96 -0
  42. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +120 -0
  43. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +59 -4
  44. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +176 -0
  45. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-state-contract.test.ts +75 -0
  46. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +131 -0
  47. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-sign.test.ts +68 -0
  48. package/payload/platform/plugins/cloudflare/mcp/__tests__/schema-exposed-dirs.test.ts +88 -0
  49. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +39 -0
  50. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +68 -1
  51. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +30 -1
  52. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/portal-sign.mjs +87 -0
  53. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +8 -4
  54. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +10 -0
  55. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +107 -1
  56. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +82 -2
  57. package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +9 -0
  58. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +56 -0
  59. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +98 -2
  60. package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +12 -0
  61. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  62. package/payload/platform/plugins/docs/references/cloudflare.md +12 -0
  63. package/payload/platform/plugins/email/PLUGIN.md +2 -1
  64. package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.d.ts +2 -0
  65. package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.d.ts.map +1 -0
  66. package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.js +92 -0
  67. package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.js.map +1 -0
  68. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.js +112 -12
  69. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.js.map +1 -1
  70. package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.d.ts +2 -0
  71. package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.d.ts.map +1 -0
  72. package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.js +54 -0
  73. package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.js.map +1 -0
  74. package/payload/platform/plugins/email/mcp/dist/__tests__/imap-drafts.test.js +170 -1
  75. package/payload/platform/plugins/email/mcp/dist/__tests__/imap-drafts.test.js.map +1 -1
  76. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.d.ts +2 -0
  77. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.d.ts.map +1 -0
  78. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.js +86 -0
  79. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.js.map +1 -0
  80. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.d.ts +2 -0
  81. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.d.ts.map +1 -0
  82. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.js +164 -0
  83. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.js.map +1 -0
  84. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.d.ts +2 -0
  85. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.d.ts.map +1 -0
  86. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.js +82 -0
  87. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.js.map +1 -0
  88. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +95 -0
  89. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  90. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +223 -0
  91. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  92. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.d.ts +18 -0
  93. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.d.ts.map +1 -0
  94. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.js +59 -0
  95. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.js.map +1 -0
  96. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.d.ts +48 -0
  97. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.d.ts.map +1 -0
  98. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.js +79 -0
  99. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.js.map +1 -0
  100. package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.d.ts +44 -0
  101. package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.d.ts.map +1 -0
  102. package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.js +63 -0
  103. package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.js.map +1 -0
  104. package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.d.ts +2 -0
  105. package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.d.ts.map +1 -0
  106. package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.js +55 -0
  107. package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.js.map +1 -0
  108. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.d.ts.map +1 -1
  109. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.js +64 -13
  110. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.js.map +1 -1
  111. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
  112. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +11 -1
  113. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
  114. package/payload/platform/plugins/email/mcp/dist/tools/email-send.d.ts.map +1 -1
  115. package/payload/platform/plugins/email/mcp/dist/tools/email-send.js +16 -3
  116. package/payload/platform/plugins/email/mcp/dist/tools/email-send.js.map +1 -1
  117. package/payload/platform/plugins/email/references/email-reference.md +36 -0
  118. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.d.ts +2 -0
  119. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.d.ts.map +1 -0
  120. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.js +66 -0
  121. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.js.map +1 -0
  122. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.d.ts +2 -0
  123. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.d.ts.map +1 -0
  124. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.js +18 -0
  125. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.js.map +1 -0
  126. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.d.ts +41 -0
  127. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.d.ts.map +1 -0
  128. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.js +75 -0
  129. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.js.map +1 -0
  130. package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.d.ts +13 -0
  131. package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.d.ts.map +1 -0
  132. package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.js +10 -0
  133. package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.js.map +1 -0
  134. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +175 -0
  135. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  136. package/payload/platform/scripts/cpu-triage-run.sh +117 -0
  137. package/payload/platform/scripts/cpu-triage.sh +23 -4
  138. package/payload/platform/services/claude-session-manager/dist/activity-range.d.ts +40 -0
  139. package/payload/platform/services/claude-session-manager/dist/activity-range.d.ts.map +1 -0
  140. package/payload/platform/services/claude-session-manager/dist/activity-range.js +68 -0
  141. package/payload/platform/services/claude-session-manager/dist/activity-range.js.map +1 -0
  142. package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts +40 -8
  143. package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts.map +1 -1
  144. package/payload/platform/services/claude-session-manager/dist/activity-rows.js +74 -7
  145. package/payload/platform/services/claude-session-manager/dist/activity-rows.js.map +1 -1
  146. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  147. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
  148. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  149. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  150. package/payload/platform/services/claude-session-manager/dist/http-server.js +9 -0
  151. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  152. package/payload/server/{chunk-56WJMBQQ.js → chunk-3XLLTG6R.js} +14 -0
  153. package/payload/server/{chunk-JECAP3Z2.js → chunk-XGNRSM57.js} +3 -0
  154. package/payload/server/maxy-edge.js +1 -1
  155. package/payload/server/public/activity.html +8 -6
  156. package/payload/server/public/assets/{AdminLoginScreens-mEKP4pDi.js → AdminLoginScreens-DkxQZnvB.js} +1 -1
  157. package/payload/server/public/assets/AdminShell-Cv_nIFGe.js +2 -0
  158. package/payload/server/public/assets/Checkbox-CsTWK8mm.js +1 -0
  159. package/payload/server/public/assets/activity-BbnTiVBT.js +1 -0
  160. package/payload/server/public/assets/{admin-BVhgfgs1.js → admin-BZY3i_Ff.js} +1 -1
  161. package/payload/server/public/assets/{browser-D-HMmHcM.js → browser-BbIiSFKr.js} +1 -1
  162. package/payload/server/public/assets/{calendar-tGWvFyFn.js → calendar-BvxeTHvQ.js} +1 -1
  163. package/payload/server/public/assets/chat-DUKxufRk.js +1 -0
  164. package/payload/server/public/assets/chevron-left-BtfJD6LL.js +1 -0
  165. package/payload/server/public/assets/data-hl1wa-Ax.js +1 -0
  166. package/payload/server/public/assets/{graph-zEw610xK.js → graph-CDSGRoeb.js} +1 -1
  167. package/payload/server/public/assets/graph-labels-C5qLwnWA.js +1 -0
  168. package/payload/server/public/assets/{maximize-2-BlTjXT_Y.js → maximize-2-CAzH8dED.js} +1 -1
  169. package/payload/server/public/assets/{operator-C7oIw2PG.js → operator-BK7r8DAB.js} +1 -1
  170. package/payload/server/public/assets/{page-CTP7OFZa.js → page-B5YngOrK.js} +1 -1
  171. package/payload/server/public/assets/{page-DL6Zsdvk.js → page-BsCS0Dmc.js} +1 -1
  172. package/payload/server/public/assets/{public-r1A9dqR_.js → public-C1Pz-IBs.js} +1 -1
  173. package/payload/server/public/assets/{rotate-ccw-CzkkKx4-.js → rotate-ccw-UDln4Tou.js} +1 -1
  174. package/payload/server/public/assets/tasks-B-cLsn3p.js +1 -0
  175. package/payload/server/public/assets/{time-entry-format-j669DgXj.js → time-entry-format-DPOBIE18.js} +1 -1
  176. package/payload/server/public/assets/{triangle-alert-BPgCgIef.js → triangle-alert-Dz-CZKo5.js} +1 -1
  177. package/payload/server/public/assets/{useCopyFeedback-BmLa2aes.js → useCopyFeedback-wtUHGohk.js} +1 -1
  178. package/payload/server/public/assets/{useSelectionMode-DnnM7A8-.js → useSelectionMode-sph7AQT5.js} +1 -1
  179. package/payload/server/public/assets/useSubAccountSwitcher-BRIluIKf.css +1 -0
  180. package/payload/server/public/assets/{useVoiceRecorder-u2dwZfen.js → useVoiceRecorder-DN4jYQ0A.js} +1 -1
  181. package/payload/server/public/brand-defaults.css +1 -0
  182. package/payload/server/public/browser.html +5 -5
  183. package/payload/server/public/calendar.html +6 -6
  184. package/payload/server/public/chat.html +12 -12
  185. package/payload/server/public/data.html +11 -11
  186. package/payload/server/public/graph.html +11 -11
  187. package/payload/server/public/index.html +14 -14
  188. package/payload/server/public/operator.html +14 -14
  189. package/payload/server/public/privacy.html +1 -0
  190. package/payload/server/public/public.html +12 -12
  191. package/payload/server/public/tasks.html +6 -5
  192. package/payload/server/public/vnc-popout.html +1 -0
  193. package/payload/server/server.js +894 -384
  194. package/payload/server/{src-JYPKMWJR.js → src-XQ63FPRE.js} +3 -1
  195. package/payload/server/public/assets/AdminShell-BwSaZ88d.js +0 -2
  196. package/payload/server/public/assets/Checkbox-CN4uQ80w.js +0 -1
  197. package/payload/server/public/assets/activity-CIQ8ozNZ.js +0 -1
  198. package/payload/server/public/assets/chat-DkttVxAz.js +0 -1
  199. package/payload/server/public/assets/chevron-left-CZ4ez9G5.js +0 -1
  200. package/payload/server/public/assets/data-P-mcmnNi.js +0 -1
  201. package/payload/server/public/assets/graph-labels-qnBleOE6.js +0 -1
  202. package/payload/server/public/assets/tasks-fIJwYFWG.js +0 -1
  203. package/payload/server/public/assets/useSubAccountSwitcher-C_E8h07P.css +0 -1
  204. /package/payload/server/public/assets/{useSubAccountSwitcher-CFa6ZAT1.js → useSubAccountSwitcher-v8LyBzZN.js} +0 -0
@@ -122,3 +122,59 @@ body {
122
122
  color: var(--dp-muted);
123
123
  padding: 0.75rem 0;
124
124
  }
125
+
126
+ /* "Shared with you" — the folder tree the device publishes. Deliberately reuses
127
+ the existing variables and adds none: portal-brand-css.mjs rewrites the one
128
+ :root block from the brand palette and validates every value it finds there,
129
+ so a new variable declared here would either be dropped or fail that check. */
130
+ .dp-shared {
131
+ margin-top: 28px;
132
+ border-top: 1px solid var(--dp-rule);
133
+ padding-top: 16px;
134
+ }
135
+
136
+ .dp-subtitle {
137
+ font-size: 15px;
138
+ font-weight: 600;
139
+ margin: 0 0 8px;
140
+ color: var(--dp-ink);
141
+ }
142
+
143
+ .dp-crumbs {
144
+ font-size: 13px;
145
+ color: var(--dp-muted);
146
+ margin-bottom: 10px;
147
+ }
148
+
149
+ /* Crumbs and folder rows are buttons (navigation is a fetch, not a page load),
150
+ so they need the button chrome stripped to sit beside the upload list's
151
+ anchors without looking like form controls. */
152
+ .dp-crumb,
153
+ .dp-folder-name {
154
+ background: none;
155
+ border: 0;
156
+ padding: 0;
157
+ font: inherit;
158
+ cursor: pointer;
159
+ color: var(--dp-accent);
160
+ }
161
+
162
+ .dp-crumb:hover,
163
+ .dp-folder-name:hover {
164
+ text-decoration: underline;
165
+ }
166
+
167
+ .dp-folder-name {
168
+ font-weight: 600;
169
+ }
170
+
171
+ /* The file rows inside the tree are buttons too — same reset, but they must
172
+ match .dp-file-name's weight so the two lists read as one surface. */
173
+ .dp-shared .dp-file-name {
174
+ background: none;
175
+ border: 0;
176
+ padding: 0;
177
+ font: inherit;
178
+ cursor: pointer;
179
+ text-align: left;
180
+ }
@@ -16,6 +16,12 @@
16
16
  var loginEl = document.getElementById('dp-login');
17
17
  var panelEl = document.getElementById('dp-panel');
18
18
  var filesEl = document.getElementById('dp-files');
19
+ var sharedEl = document.getElementById('dp-shared');
20
+ var crumbsEl = document.getElementById('dp-crumbs');
21
+ var entriesEl = document.getElementById('dp-entries');
22
+
23
+ /* The folder currently open in the "shared with you" tree; '' is its root. */
24
+ var prefix = '';
19
25
 
20
26
  function say(msg) {
21
27
  statusEl.textContent = msg;
@@ -64,13 +70,103 @@
64
70
  });
65
71
  }
66
72
 
73
+ /* One crumb. A button rather than a link: navigating the tree is a fetch, not
74
+ a page load, and the session cookie stays where it is. */
75
+ function crumb(label, target) {
76
+ var b = document.createElement('button');
77
+ b.className = 'dp-crumb';
78
+ b.type = 'button';
79
+ b.textContent = label;
80
+ b.addEventListener('click', function () {
81
+ prefix = target;
82
+ refresh();
83
+ });
84
+ return b;
85
+ }
86
+
87
+ function renderCrumbs() {
88
+ crumbsEl.replaceChildren();
89
+ crumbsEl.appendChild(crumb('All folders', ''));
90
+ var parts = prefix ? prefix.split('/') : [];
91
+ parts.forEach(function (part, i) {
92
+ crumbsEl.appendChild(document.createTextNode(' / '));
93
+ crumbsEl.appendChild(crumb(part, parts.slice(0, i + 1).join('/')));
94
+ });
95
+ }
96
+
97
+ /* Every label goes through textContent, same rule as the upload list: these
98
+ names come off the device and are never trusted as markup. */
99
+ function renderEntries(entries) {
100
+ renderCrumbs();
101
+ entriesEl.replaceChildren();
102
+ if (!entries.length) {
103
+ entriesEl.appendChild(item('Nothing here.'));
104
+ return;
105
+ }
106
+ entries.forEach(function (e) {
107
+ var li = document.createElement('li');
108
+ li.className = 'dp-file-row';
109
+
110
+ var b = document.createElement('button');
111
+ b.className = e.kind === 'directory' ? 'dp-folder-name' : 'dp-file-name';
112
+ b.type = 'button';
113
+ b.textContent = e.kind === 'directory' ? e.name + '/' : e.name;
114
+ b.addEventListener('click', function () {
115
+ if (e.kind === 'directory') {
116
+ prefix = e.relPath;
117
+ refresh();
118
+ } else {
119
+ openShared(e.relPath);
120
+ }
121
+ });
122
+ li.appendChild(b);
123
+ entriesEl.appendChild(li);
124
+ });
125
+ }
126
+
127
+ /* These files live on the device, not in the portal's own store, so the
128
+ portal checks the device is reachable and then hands back a link that
129
+ expires. A device that is down says so plainly instead of failing as a
130
+ broken download. */
131
+ function openShared(relPath) {
132
+ say('Opening…');
133
+ api('/api/download?path=' + encodeURIComponent(relPath))
134
+ .then(function (r) {
135
+ return r.json().then(function (d) {
136
+ return { s: r.status, d: d };
137
+ });
138
+ })
139
+ .then(function (res) {
140
+ if (res.s === 503) {
141
+ say('That file is on the device, which is not reachable right now. Try again shortly.');
142
+ return;
143
+ }
144
+ if (!res.d.ok || !res.d.url) {
145
+ say('That file could not be opened.');
146
+ return;
147
+ }
148
+ say('');
149
+ window.location.assign(res.d.url);
150
+ })
151
+ .catch(function () {
152
+ say('That file could not be opened.');
153
+ });
154
+ }
155
+
67
156
  function refresh() {
68
- return api('/api/files')
157
+ return api('/api/files?prefix=' + encodeURIComponent(prefix))
69
158
  .then(function (r) {
70
159
  return r.json();
71
160
  })
72
161
  .then(function (d) {
73
- if (d.ok) render(d.files || []);
162
+ if (!d.ok) return;
163
+ render(d.files || []);
164
+ var entries = d.entries || [];
165
+ // Hidden at the root only: once inside a folder the section stays put,
166
+ // so an empty folder reads as empty rather than as the whole tree
167
+ // vanishing.
168
+ sharedEl.hidden = prefix === '' && entries.length === 0;
169
+ renderEntries(entries);
74
170
  })
75
171
  .catch(function () {
76
172
  say('Could not load your files.');
@@ -4,6 +4,7 @@
4
4
  # __D1_DATABASE_NAME__ the D1 database name (<accountId>-<purpose>)
5
5
  # __D1_DATABASE_ID__ the id printed by `wrangler d1 create`
6
6
  # __R2_BUCKET_NAME__ the R2 bucket name (<accountId>-<purpose>)
7
+ # __PORTAL_INSTALL_ORIGIN__ the install's public origin (https://<tunnel-hostname>)
7
8
  name = "__PROJECT_NAME__"
8
9
  pages_build_output_dir = "."
9
10
  compatibility_date = "2024-01-01"
@@ -19,3 +20,14 @@ database_id = "__D1_DATABASE_ID__"
19
20
  [[r2_buckets]]
20
21
  binding = "BUCKET"
21
22
  bucket_name = "__R2_BUCKET_NAME__"
23
+
24
+ # Where the install answers, so a download of a device-held file can be signed
25
+ # and sent there. Public information — it is the hostname a client's browser
26
+ # ends up at.
27
+ #
28
+ # Its partner, PORTAL_FETCH_SECRET, is deliberately NOT here. This file is
29
+ # committed to the account's assembled tree and the deploy publishes the
30
+ # directory it is handed, so a secret in it is a secret in a repo and one deploy
31
+ # from being served. Set it with `wrangler pages secret put`.
32
+ [vars]
33
+ PORTAL_INSTALL_ORIGIN = "__PORTAL_INSTALL_ORIGIN__"
@@ -65,7 +65,7 @@ either is a regression.
65
65
 
66
66
  **Row title resolution.** Both the sidebar (`/sidebar-sessions`) and the manager's own row payload resolve a row's title in the same order: operator rename → Claude Code `ai-title` → first non-CLI user message → 8-char sessionId prefix. The operator-rename tier is the on-disk `<accountDir>/session-titles.json` (the manager's `UserTitleStore`), keyed by the CC sessionId — the sidebar reads that same file, so a write to the store lights up both surfaces with one write.
67
67
 
68
- **Session row delete, rename, and select mode.** The sidebar Sessions row supports three operator affordances. **Rename** (kebab → Rename) opens the inline edit input with its existing title pre-selected on focus, so the first keystroke replaces the whole title rather than appending to it — on both the sidebar (`conv-name-edit`) and the `/chat` Conversations flyout (`op-conv-rename-input`). **Delete** (kebab → Delete) arms a confirm that names the session (its label plus the 8-char id prefix) and renders as a popover anchored beside the row it will delete — reusing the same `AnchoredPopover` the kebab menu uses, not the shared bottom-right info dock — so the target is unambiguous; it still states the delete is permanent and, for a live row, that the session is stopped first. **Select mode** (a "Select" control in the Sessions header) turns each row into a checkbox and shows a "Delete N" bar; the row body toggles selection instead of opening, the per-row kebab is hidden, and confirming a count-based `BulkDeleteConfirmModal` (which names N and how many are live) loops the single-id `session-delete` over the selection (no route change — `/api/admin/session-delete` stays single-id). Bulk deletion is instrumented with a `[admin-ui] sidebar-bulk-delete op=start count=<N> live=<M>` / `op=done requested=<N> ok=<X> failed=<Y>` envelope around the per-session `sidebar-session-delete` lines, so a silently-dropped item shows as `ok+failed < requested`. Outside select mode the single-delete and row-open behaviours are unchanged.
68
+ **Session row delete, rename, and select mode.** The sidebar Sessions row supports three operator affordances. **Rename** (kebab → Rename) opens the inline edit input with its existing title pre-selected on focus, so the first keystroke replaces the whole title rather than appending to it — on both the sidebar (`conv-name-edit`) and the `/chat` Conversations flyout (`op-conv-rename-input`). **Delete** (kebab → Delete) arms a confirm that names the session (its label plus the 8-char id prefix) and renders as a popover anchored beside the row it will delete — reusing the same `AnchoredPopover` the kebab menu uses, not the shared bottom-right info dock — so the target is unambiguous; it still states the delete is permanent and, for a live row, that the session is stopped first. **Usage & cost** (kebab → Usage & cost) opens the per-day active-time / token / GBP-cost table for that session, and anchors beside its row through the same `AnchoredPopover`, with no scrim, so the rest of the dashboard stays legible and interactive while it is open. `AnchoredPopover` right-aligns to the anchor's rect and clamps horizontally against both viewport edges. Vertically it places below when the popover fits there, above when it fits there instead, and otherwise on whichever side has more room — and in every case it bounds the popover's height to the room on the side it chose. That bound is what keeps a long session's panel anchored: a 30-day usage table is taller than the viewport, and clamping it to fit would push it up over the very row it describes, which is not "anchored" to anything. Instead it sits flush against its row and scrolls internally. Because the bound is derived from the anchor rect and the viewport rather than from the popover's own height, the `ResizeObserver` pass it triggers recomputes the same values and settles in one extra pass. The horizontal right-edge clamp does not engage for a left-docked sidebar row (a popover wider than its anchor overflows left, which the left clamp handles); it is kept because the component is general-purpose and is covered by unit test only. Positioning is instrumented, because a mispositioned client-side panel throws nothing and the operator just sees a menu item that did nothing: every positioning pass (each open, each `ResizeObserver` callback, each window resize) emits `[anchored-popover] op=anchor id=<class>:<sessionId> present=<bool> rect=<x,y,w,h>` then `op=placed … maxH=<px> flipped=<bool> clampedLeft=<bool> clampedRight=<bool>`, and `op=offscreen` at `console.error` when the anchor is detached from layout or a placed rect misses the viewport. A pass that computes exactly what the previous pass did is suppressed, so mount and its trailing `ResizeObserver` callback log once rather than twice; the `op=offscreen` assertion is never suppressed. A healthy open is `present=true` with a non-zero rect followed by `op=placed` and no `op=offscreen`. `present=false` means the row is not in `rowRefs` — the row unmounted or its ref callback did not run — and the panel docks bottom-right rather than vanishing, because an invisible panel is indistinguishable from a broken menu item. An all-zero rect means the anchor is detached. The assertion is gated on the popover having non-zero measured size, so an unlaid-out popover (pre-paint, or jsdom) stays silent; a genuinely zero-size popover in production is therefore not reported. **Select mode** (a "Select" control in the Sessions header) turns each row into a checkbox and shows a "Delete N" bar; the row body toggles selection instead of opening, the per-row kebab is hidden, and confirming a count-based `BulkDeleteConfirmModal` (which names N and how many are live) loops the single-id `session-delete` over the selection (no route change — `/api/admin/session-delete` stays single-id). Bulk deletion is instrumented with a `[admin-ui] sidebar-bulk-delete op=start count=<N> live=<M>` / `op=done requested=<N> ok=<X> failed=<Y>` envelope around the per-session `sidebar-session-delete` lines, so a silently-dropped item shows as `ok+failed < requested`. Outside select mode the single-delete and row-open behaviours are unchanged.
69
69
 
70
70
  **`/chat` empty-state starter chips.** While the admin webchat has no conversation JSONL yet (`GET /api/webchat/session` → `projectDir:null`), `app/chat/page.tsx` renders a "What's up next, {givenName}?" greeting (given name = first whitespace token of the admin session's `userName`; degrades to "What's up next?" without one) above three one-tap starter chips (replacing the earlier read-only lists): **Catch me up** (`Inbox`), **My day** (`CalendarClock`), **To-do** (`ListTodo`). `GreetingPanel` no longer fetches `GET /api/webchat/greeting` — the headline reads from the `userName` prop alone. A tap dispatches the chip's verbatim prompt through the existing `send()` with zero typing, via the same arm/fire path as the `?q=` handoff seed: `dispatchChip` logs `[admin-ui] op=chip-tap id=<catch-up|my-day|to-do> chars=<n>`, sets the composer text, and arms a one-shot effect that fires `send()` once the text matches. Chips gate to `variant` admin/operator; the public surface shows the headline alone. The first journaled turn replaces the panel with the transcript. The `/api/webchat/greeting` route (`server/routes/webchat-greeting.ts`) and `specialist-roster.ts` are now unconsumed by this surface — slimming them is a separate follow-up. The full webchat architecture lives in `.docs/admin-webchat-native-channel.md`.
71
71
 
@@ -101,6 +101,18 @@ The most common cause is wrong nameservers on the domain. The domain must use Cl
101
101
  - The `/data` page handles this itself: files over 95 MB are split into ~48 MB parts and sent one at a time under the cap, and the server reassembles them. No plan change or dashboard action is needed for uploads up to the 2 GB app ceiling.
102
102
  - Raising the proxied body limit is a paid-plan change made on the Cloudflare dashboard, not something the agent does.
103
103
 
104
+ ## The client file portal
105
+
106
+ A portal gives named people their own passcode-protected page on a custom domain. Two things sit on it.
107
+
108
+ **What they send you.** Anything they upload lands in the account's own storage, one area per person, and nobody sees anyone else's files.
109
+
110
+ **What you have prepared for them.** The same page shows the account's deliverable folders — the work produced for that client. Which folders those are is not a setting to maintain: it follows the account's own filing structure, so a business whose work is organised into quotes sees quotes, and a business organised some other way sees that instead. Operator working data, the client's own uploads, and anything internal are never shown.
111
+
112
+ Folder names and sizes are published to the portal on a schedule, so the client can browse even while the device is off. The files themselves stay on the device. Opening one checks the device is reachable and then hands the browser a link that expires after a few minutes; if the device is down the client is told exactly that, rather than getting a broken download.
113
+
114
+ Each person is bound to one account when they are enrolled. Someone enrolled before this existed has no account bound and sees no folders until they are enrolled again.
115
+
104
116
  ## What the agent does and does not do
105
117
 
106
118
  **Does:** invokes `cloudflared` directly via Bash, following `plugins/cloudflare/references/manual-setup.md` step by step; quotes click-paths from the reference files verbatim; verifies external reachability with `curl -I` and surfaces the response.
@@ -89,7 +89,8 @@ Manages the agent's own dedicated email account — IMAP for reading, SMTP for s
89
89
  - **Reply:** `email-reply` — threaded reply to an existing email by `messageId`. Resolves the original envelope via IMAP `SEARCH HEADER Message-ID` (INBOX then `\Sent`), so threading works for any message still present on the server even if the graph never ingested it. Supports `replyAll`, `cc`/`bcc`, and `attachments` (same path rules as `email-send`). Explicit `cc`/`bcc` are added in addition to whatever `replyAll` places in `To`, passed through as given (no overlap de-duplication against `To`).
90
90
  - **Compose to Drafts:** `email-draft` — builds the same MIME message as `email-send` (same `cc`/`bcc`/`attachments` rules; unlike a dispatched send, the stored draft keeps its `Bcc:` header, because the draft bytes are the only recipient carrier) but APPENDs it to the mailbox's `\Drafts` special-use folder with the `\Draft` flag instead of dispatching it. Nothing is sent. Pass `messageId` to compose a threaded draft-reply: the parent is resolved via IMAP and the draft carries `In-Reply-To`/`References` plus a `Re:` subject from the parent, exactly like `email-reply` (the `subject` argument is then optional and ignored). The success message names the Drafts folder, the APPEND UID, and the draft's own `Message-ID` (`draft id`) so a saved draft is never indistinguishable from a silent no-op and a later edit has a stable handle. Fails naming the mailbox when the server advertises no `\Drafts` special-use folder — no fallback folder is picked, unless a per-account `draftsFolder` override is configured (via `email-setup`), which is honoured before special-use discovery for hosts whose client-visible Drafts is a differently-named mailbox. Use when the operator wants to review and send by hand.
91
91
  - **Revise a draft:** `email-draft-edit` — replace a stored draft. The `targetUid` is a fast-path hint; the draft's own `Message-ID` (`draftMessageId`) is the stable identity, because Gmail renumbers a draft's UID when it is touched. The tool tries the UID first, then relocates by a `Message-ID` header search, then composes the replacement from the parameters (same shape as `email-draft`, plus `targetUid`, optional `draftMessageId`, and an optional `messageId` for a threaded revision), APPENDs it with `\Draft`, and deletes the resolved old copy — the replacement is always appended before the old copy is expunged, so a mid-cycle failure never leaves zero copies. When neither identity resolves a live draft, the prior draft is gone: the replacement is saved as a fresh draft (`created`) rather than failing. A single transient transport drop is retried on a fresh connection; auth and other permanent errors surface immediately. Nothing is sent. The confirmation names the folder, the new UID, and the new draft id.
92
- - **Send a draft:** `email-draft-send` — dispatch a stored draft addressed by its Drafts UID via SMTP, then remove it from Drafts. Recipients come from the draft's own To/Cc/Bcc headers (the `\Drafts` copy keeps its `Bcc:`), deduped, sent from the agent alias with an explicit SMTP envelope. The draft is deleted only after SMTP accepts; an SMTP failure leaves it in place and surfaces verbatim. A draft with no recipients is refused before sending. Operator-approved sends only.
92
+ - **Send a draft:** `email-draft-send` — dispatch a stored draft addressed by its Drafts UID via SMTP, then relocate it into `\Sent`. Recipients come from the draft's own To/Cc/Bcc headers (the `\Drafts` copy keeps its `Bcc:`), deduped, sent from the agent alias with an explicit SMTP envelope. The draft is relocated only after SMTP accepts; an SMTP failure leaves it in place and surfaces verbatim. A draft with no recipients is refused before sending. Operator-approved sends only.
93
+ - **The Sent copy is verified, not assumed.** All three send paths read `\Sent` back after SMTP acceptance and search it by Message-ID, then state what is actually there as a fact separate from the send. A folder and UID are named only when they were read back; "no copy was found", "this server advertises no `\Sent` folder", and "the Sent folder could not be checked" are reported as three distinct outcomes, never collapsed. A send whose copy cannot be located is still a successful send. Each send also appends its observed outcome to `config/email-sent-ledger.jsonl`, and the install heartbeat spawns `dist/scripts/sent-copy-sweep.js` hourly to ask `\Sent`, per mailbox, whether each Message-ID sent in the trailing 24 h is actually present (`[email-sent-sweep] op=reconcile …`). Presence is asked per Message-ID rather than by comparing folder totals, because `\Sent` also holds everything the owner sends from other clients. See [`references/email-reference.md`](references/email-reference.md).
93
94
  - **Ingestion — fetch:** `email-fetch` — list new IMAP messages since the stored high-water mark. Metadata + body preview only — including attachment names, mimetypes, and sizes so the operator sees what's attached before approving. No bytes downloaded, no graph writes. Stages the batch in-memory for `email-ingest`.
94
95
  - **Ingestion — apply:** `email-ingest` — apply the operator's per-message decisions to the staged batch. Each Message-ID must carry a disposition (`ingest` or `discard`). Approved messages land on `:ConversationArchive {source:'email'}` via the source-agnostic conversation-archive pipeline (one archive per thread, sessionised into `:Section` chunks). Attachment bytes are fetched, archived to `{accountDir}/archive/email/<uidValidity>-<uid>/<sha256>-<filename>` (0o600, 25 MB cap), and each attachment lands as a content-addressed `:DigitalDocument` keyed on `attachmentId = sha256(bytes)`; the `:HAS_ENCLOSURE` edge from the parent `:ConversationArchive` is wired by `memory-ingest` on its next pass via the `pendingArchiveEdges` queue. Advances the inbox high-water mark only after a successful write.
95
96
  - **Recall/history:** `email-graph-query` — search stored email archives in Neo4j by natural language, participant address, or date range. Each thread is one `:ConversationArchive {source:'email'}` with `:Section` chunks; operator participants attach via `:PARTICIPANT_IN` edges from `:Person`/`:AdminUser`. List mode surfaces a per-archive summary built from concatenated `:Section.summary` values; query mode runs vector search over the `section_embedding` index and dedups to the parent archive.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=confirm-sent-copy.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confirm-sent-copy.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/confirm-sent-copy.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ const mockClient = vi.hoisted(() => ({
3
+ connect: vi.fn(),
4
+ list: vi.fn(),
5
+ getMailboxLock: vi.fn(),
6
+ search: vi.fn(),
7
+ logout: vi.fn(),
8
+ close: vi.fn(),
9
+ }));
10
+ vi.mock("imapflow", () => ({
11
+ // A constructible function — `new ImapFlow(...)` returns the spy client.
12
+ ImapFlow: function ImapFlow() {
13
+ return mockClient;
14
+ },
15
+ }));
16
+ const config = {
17
+ email: "a@example.com",
18
+ agentAddress: "a@example.com",
19
+ imapHost: "imap.example.com",
20
+ imapPort: 993,
21
+ imapSecurity: "tls",
22
+ smtpHost: "smtp.example.com",
23
+ smtpPort: 465,
24
+ smtpSecurity: "tls",
25
+ };
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ mockClient.connect.mockResolvedValue(undefined);
29
+ mockClient.logout.mockResolvedValue(undefined);
30
+ mockClient.getMailboxLock.mockResolvedValue({ release: vi.fn() });
31
+ mockClient.list.mockResolvedValue([
32
+ { path: "INBOX", specialUse: undefined },
33
+ { path: "[Gmail]/Sent Mail", specialUse: "\\Sent" },
34
+ ]);
35
+ });
36
+ describe("confirmSentCopy", () => {
37
+ it("reports the folder and UID when the copy is found", async () => {
38
+ const { confirmSentCopy } = await import("../lib/imap.js");
39
+ mockClient.search.mockResolvedValue([42]);
40
+ const result = await confirmSentCopy(config, "pw", "abc@example.com");
41
+ expect(result).toEqual({ found: true, sentFolder: "[Gmail]/Sent Mail", sentUid: 42 });
42
+ });
43
+ it("searches the raw header value with angle brackets restored", async () => {
44
+ const { confirmSentCopy } = await import("../lib/imap.js");
45
+ mockClient.search.mockResolvedValue([7]);
46
+ await confirmSentCopy(config, "pw", "abc@example.com");
47
+ expect(mockClient.search).toHaveBeenCalledWith({ header: { "message-id": "<abc@example.com>" } }, { uid: true });
48
+ });
49
+ it("reports not-in-sent with the folder named when the search stays empty", async () => {
50
+ const { confirmSentCopy } = await import("../lib/imap.js");
51
+ mockClient.search.mockResolvedValue([]);
52
+ const result = await confirmSentCopy(config, "pw", "abc@example.com", {
53
+ attempts: 2,
54
+ delayMs: 0,
55
+ });
56
+ expect(result).toEqual({
57
+ found: false,
58
+ reason: "not-in-sent",
59
+ sentFolder: "[Gmail]/Sent Mail",
60
+ });
61
+ });
62
+ it("retries on provider filing lag and reports found when a later attempt hits", async () => {
63
+ const { confirmSentCopy } = await import("../lib/imap.js");
64
+ mockClient.search.mockResolvedValueOnce([]).mockResolvedValueOnce([99]);
65
+ const result = await confirmSentCopy(config, "pw", "abc@example.com", {
66
+ attempts: 3,
67
+ delayMs: 0,
68
+ });
69
+ expect(result).toEqual({ found: true, sentFolder: "[Gmail]/Sent Mail", sentUid: 99 });
70
+ expect(mockClient.search).toHaveBeenCalledTimes(2);
71
+ });
72
+ it("reports no-sent-folder when the server advertises none", async () => {
73
+ const { confirmSentCopy } = await import("../lib/imap.js");
74
+ mockClient.list.mockResolvedValue([{ path: "INBOX", specialUse: undefined }]);
75
+ const result = await confirmSentCopy(config, "pw", "abc@example.com");
76
+ expect(result).toEqual({ found: false, reason: "no-sent-folder" });
77
+ expect(mockClient.search).not.toHaveBeenCalled();
78
+ });
79
+ it("reports check-failed rather than throwing when the connection dies", async () => {
80
+ const { confirmSentCopy } = await import("../lib/imap.js");
81
+ mockClient.connect.mockRejectedValueOnce(new Error("ECONNREFUSED"));
82
+ const result = await confirmSentCopy(config, "pw", "abc@example.com");
83
+ expect(result).toEqual({ found: false, reason: "check-failed", error: "ECONNREFUSED" });
84
+ });
85
+ it("logs out even when the search throws", async () => {
86
+ const { confirmSentCopy } = await import("../lib/imap.js");
87
+ mockClient.search.mockRejectedValue(new Error("boom"));
88
+ await confirmSentCopy(config, "pw", "abc@example.com");
89
+ expect(mockClient.logout).toHaveBeenCalled();
90
+ });
91
+ });
92
+ //# sourceMappingURL=confirm-sent-copy.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confirm-sent-copy.test.js","sourceRoot":"","sources":["../../src/__tests__/confirm-sent-copy.test.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,12 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
- const { fetchMock, sendRawMock, deleteMock } = vi.hoisted(() => ({
2
+ const { fetchMock, sendRawMock, fileMock, ledgerMock } = vi.hoisted(() => ({
3
3
  fetchMock: vi.fn(),
4
4
  sendRawMock: vi.fn(),
5
- deleteMock: vi.fn(),
5
+ fileMock: vi.fn(),
6
+ ledgerMock: vi.fn(),
6
7
  }));
7
- vi.mock("../lib/imap.js", () => ({ fetchDraftSource: fetchMock, deleteDraft: deleteMock }));
8
+ vi.mock("../lib/imap.js", () => ({ fetchDraftSource: fetchMock, fileDraftToSent: fileMock }));
9
+ vi.mock("../lib/sent-ledger.js", () => ({ appendSentLedger: ledgerMock }));
8
10
  vi.mock("../lib/smtp.js", async (importOriginal) => {
9
11
  const actual = await importOriginal();
10
12
  return { ...actual, sendRaw: sendRawMock };
@@ -27,36 +29,134 @@ describe("Task 819 — email-draft-send", () => {
27
29
  vi.clearAllMocks();
28
30
  fetchMock.mockResolvedValue({ ...draft });
29
31
  sendRawMock.mockResolvedValue({ messageId: "<sent@x>" });
30
- deleteMock.mockResolvedValue({ folder: "Drafts", deleted: true });
32
+ fileMock.mockResolvedValue({
33
+ filed: true,
34
+ sentFolder: "[Gmail]/Sent Mail",
35
+ sentUid: 993,
36
+ draftFolder: "[Gmail]/Drafts",
37
+ });
31
38
  });
32
- it("sends raw with a To+Cc+Bcc envelope, THEN removes the draft", async () => {
39
+ it("sends raw with a To+Cc+Bcc envelope, THEN files the copy to Sent", async () => {
33
40
  const text = await emailDraftSend({ accountId: "acc-1", uid: 5 });
34
41
  expect(sendRawMock).toHaveBeenCalledWith(expect.anything(), "pw", draft.source, {
35
42
  from: "a@x.y",
36
43
  to: ["c@d.e", "f@g.h", "i@j.k"],
37
44
  });
38
- expect(deleteMock).toHaveBeenCalledWith(expect.anything(), "pw", 5);
45
+ expect(fileMock).toHaveBeenCalledWith(expect.anything(), "pw", 5, "<sent@x>");
39
46
  expect(text).toMatch(/sent to/i);
40
47
  expect(text).toContain("<sent@x>");
41
- expect(text).toContain("removed");
48
+ expect(text).toContain("copy confirmed in [Gmail]/Sent Mail (UID 993)");
49
+ });
50
+ it("records the confirmed copy in the sent ledger", async () => {
51
+ await emailDraftSend({ accountId: "acc-1", uid: 5 });
52
+ expect(ledgerMock).toHaveBeenCalledWith(expect.objectContaining({
53
+ accountId: "acc-1",
54
+ mailbox: "a@x.y",
55
+ messageId: "<sent@x>",
56
+ copy: "found",
57
+ sentFolder: "[Gmail]/Sent Mail",
58
+ sentUid: 993,
59
+ }));
60
+ });
61
+ it("records an unfiled send as not-in-sent rather than skipping the row", async () => {
62
+ fileMock.mockResolvedValue({
63
+ filed: false,
64
+ reason: "moved-but-not-in-sent",
65
+ draftFolder: "[Gmail]/Drafts",
66
+ });
67
+ await emailDraftSend({ accountId: "acc-1", uid: 5 });
68
+ expect(ledgerMock).toHaveBeenCalledWith(expect.objectContaining({ messageId: "<sent@x>", copy: "not-in-sent" }));
69
+ });
70
+ // Each outcome keeps its own ledger value: "not-in-sent" asserts the folder
71
+ // was searched and was empty, which is untrue of both cases below.
72
+ it("records a server with no Sent folder as no-sent-folder, not not-in-sent", async () => {
73
+ fileMock.mockResolvedValue({
74
+ filed: false,
75
+ reason: "no-sent-folder",
76
+ draftFolder: "[Gmail]/Drafts",
77
+ });
78
+ await emailDraftSend({ accountId: "acc-1", uid: 5 });
79
+ expect(ledgerMock).toHaveBeenCalledWith(expect.objectContaining({ copy: "no-sent-folder" }));
80
+ });
81
+ it("records a thrown filing as check-failed, not as a confirmed absence", async () => {
82
+ fileMock.mockRejectedValue(new Error("ECONNREFUSED"));
83
+ await emailDraftSend({ accountId: "acc-1", uid: 5 });
84
+ expect(ledgerMock).toHaveBeenCalledWith(expect.objectContaining({ copy: "check-failed" }));
85
+ });
86
+ it("writes no ledger row when the server returned no Message-ID", async () => {
87
+ sendRawMock.mockResolvedValue({ messageId: null });
88
+ await emailDraftSend({ accountId: "acc-1", uid: 5 });
89
+ expect(ledgerMock).not.toHaveBeenCalled();
90
+ });
91
+ // Task 1822 removed the "filed, UID not reported by the server" branch: a
92
+ // filed result now always carries a UID that was read back, so a move the
93
+ // library could not confirm arrives here as moved-but-not-in-sent instead.
94
+ it("reports a move it could not confirm without claiming a copy was filed", async () => {
95
+ fileMock.mockResolvedValue({
96
+ filed: false,
97
+ reason: "moved-but-not-in-sent",
98
+ draftFolder: "[Gmail]/Drafts",
99
+ });
100
+ const text = await emailDraftSend({ accountId: "acc-1", uid: 5 });
101
+ expect(text).toMatch(/sent to/i);
102
+ expect(text).toContain("no copy was found in the Sent folder");
103
+ expect(text).not.toMatch(/copy confirmed/);
104
+ expect(text).not.toMatch(/UID null|UID undefined/);
105
+ });
106
+ it("reports where the draft still is when the server advertises no \\Sent folder", async () => {
107
+ fileMock.mockResolvedValue({
108
+ filed: false,
109
+ reason: "no-sent-folder",
110
+ draftFolder: "[Gmail]/Drafts",
111
+ });
112
+ const text = await emailDraftSend({ accountId: "acc-1", uid: 5 });
113
+ expect(text).toMatch(/sent to/i);
114
+ expect(text).toContain("no copy was filed");
115
+ expect(text).toContain("still in [Gmail]/Drafts as UID 5");
116
+ expect(text).not.toMatch(/copy filed to/);
117
+ });
118
+ it("reports a MOVE that moved nothing without claiming a copy was filed", async () => {
119
+ fileMock.mockResolvedValue({
120
+ filed: false,
121
+ reason: "move-did-nothing",
122
+ draftFolder: "[Gmail]/Drafts",
123
+ });
124
+ const text = await emailDraftSend({ accountId: "acc-1", uid: 5 });
125
+ expect(text).toMatch(/sent to/i);
126
+ expect(text).toContain("the server moved nothing");
127
+ expect(text).toContain("still in [Gmail]/Drafts as UID 5");
128
+ expect(text).not.toMatch(/copy filed to/);
129
+ });
130
+ it("reports draft-absent without claiming a copy was filed", async () => {
131
+ fileMock.mockResolvedValue({
132
+ filed: false,
133
+ reason: "draft-absent",
134
+ draftFolder: "[Gmail]/Drafts",
135
+ });
136
+ const text = await emailDraftSend({ accountId: "acc-1", uid: 5 });
137
+ expect(text).toContain("no copy was filed");
138
+ expect(text).toContain("draft UID 5 was no longer in [Gmail]/Drafts");
139
+ expect(text).not.toMatch(/copy filed to/);
42
140
  });
43
141
  it("missing draft throws and never sends", async () => {
44
142
  fetchMock.mockResolvedValueOnce(null);
45
143
  await expect(emailDraftSend({ accountId: "acc-1", uid: 404 })).rejects.toThrow(/No draft with UID 404/);
46
144
  expect(sendRawMock).not.toHaveBeenCalled();
47
145
  });
48
- it("a removal failure AFTER a successful send still reports success with a caveat", async () => {
49
- deleteMock.mockRejectedValueOnce(new Error("Draft UID 5 not found in Drafts."));
146
+ it("a filing failure AFTER a successful send still reports success with a caveat", async () => {
147
+ fileMock.mockRejectedValueOnce(new Error("connection dropped"));
50
148
  const text = await emailDraftSend({ accountId: "acc-1", uid: 5 });
51
149
  expect(sendRawMock).toHaveBeenCalledTimes(1);
52
150
  expect(text).toMatch(/sent to/i);
53
151
  expect(text).toContain("<sent@x>");
54
- expect(text).toMatch(/could not be confirmed removed/);
152
+ expect(text).toContain("still in Drafts as UID 5");
153
+ expect(text).toContain("connection dropped");
154
+ expect(text).not.toMatch(/copy filed to/);
55
155
  });
56
- it("SMTP failure leaves the draft in place (no delete) and surfaces verbatim", async () => {
156
+ it("SMTP failure leaves the draft in place (no filing) and surfaces verbatim", async () => {
57
157
  sendRawMock.mockRejectedValueOnce(new Error("SMTP 550 rejected"));
58
158
  await expect(emailDraftSend({ accountId: "acc-1", uid: 5 })).rejects.toThrow(/SMTP 550/);
59
- expect(deleteMock).not.toHaveBeenCalled();
159
+ expect(fileMock).not.toHaveBeenCalled();
60
160
  });
61
161
  it("a draft with no recipients throws before sending", async () => {
62
162
  fetchMock.mockResolvedValueOnce({
@@ -1 +1 @@
1
- {"version":3,"file":"email-draft-send.test.js","sourceRoot":"","sources":["../../src/__tests__/email-draft-send.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9D,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/D,SAAS,EAAE,EAAE,CAAC,EAAE,EAAE;IAClB,WAAW,EAAE,EAAE,CAAC,EAAE,EAAE;IACpB,UAAU,EAAE,EAAE,CAAC,EAAE,EAAE;CACpB,CAAC,CAAC,CAAC;AACJ,EAAE,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,gBAAgB,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;AAC5F,EAAE,CAAC,IAAI,CAAC,gBAAgB,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE;IACjD,MAAM,MAAM,GAAG,MAAM,cAAc,EAAmC,CAAC;IACvE,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;AAC7C,CAAC,CAAC,CAAC;AACH,EAAE,CAAC,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE,CAAC,CAAC;IACtC,eAAe,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QAClC,WAAW,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;KACnF,CAAC,CAAC;CACJ,CAAC,CAAC,CAAC;AAEJ,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAE9D,MAAM,KAAK,GAAG;IACZ,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC;IACjC,WAAW,EAAE,CAAC,OAAO,CAAC;IACtB,WAAW,EAAE,CAAC,OAAO,CAAC;IACtB,YAAY,EAAE,CAAC,OAAO,CAAC;CACxB,CAAC;AAEF,QAAQ,CAAC,6BAA6B,EAAE,GAAG,EAAE;IAC3C,UAAU,CAAC,GAAG,EAAE;QACd,EAAE,CAAC,aAAa,EAAE,CAAC;QACnB,SAAS,CAAC,iBAAiB,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;QAC1C,WAAW,CAAC,iBAAiB,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;QACzD,UAAU,CAAC,iBAAiB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAClE,MAAM,CAAC,WAAW,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE;YAC9E,IAAI,EAAE,OAAO;YACb,EAAE,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;SAChC,CAAC,CAAC;QACH,MAAM,CAAC,UAAU,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACpE,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,SAAS,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC;QACtC,MAAM,MAAM,CAAC,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC5E,uBAAuB,CACxB,CAAC;QACF,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+EAA+E,EAAE,KAAK,IAAI,EAAE;QAC7F,UAAU,CAAC,qBAAqB,CAAC,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC,CAAC;QAChF,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAClE,MAAM,CAAC,WAAW,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,gCAAgC,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0EAA0E,EAAE,KAAK,IAAI,EAAE;QACxF,WAAW,CAAC,qBAAqB,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;QAClE,MAAM,MAAM,CAAC,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACzF,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,SAAS,CAAC,qBAAqB,CAAC;YAC9B,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;YACxB,WAAW,EAAE,EAAE;YACf,WAAW,EAAE,EAAE;YACf,YAAY,EAAE,EAAE;SACjB,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QAC/F,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,SAAS,CAAC,qBAAqB,CAAC;YAC9B,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;YACxB,WAAW,EAAE,CAAC,OAAO,CAAC;YACtB,WAAW,EAAE,CAAC,OAAO,CAAC;YACtB,YAAY,EAAE,EAAE;SACjB,CAAC,CAAC;QACH,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,CAAC,WAAW,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;YACpF,IAAI,EAAE,OAAO;YACb,EAAE,EAAE,CAAC,OAAO,CAAC;SACd,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oFAAoF,EAAE,KAAK,IAAI,EAAE;QAClG,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,uBAAuB,CAAC,CAAC;QACjE,eAAuD,CAAC,qBAAqB,CAAC;YAC7E,WAAW,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;SAC3F,CAAC,CAAC;QACH,WAAW,CAAC,qBAAqB,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;QACzD,MAAM,MAAM,CAAC,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC1E,uDAAuD,CACxD,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"email-draft-send.test.js","sourceRoot":"","sources":["../../src/__tests__/email-draft-send.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9D,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACzE,SAAS,EAAE,EAAE,CAAC,EAAE,EAAE;IAClB,WAAW,EAAE,EAAE,CAAC,EAAE,EAAE;IACpB,QAAQ,EAAE,EAAE,CAAC,EAAE,EAAE;IACjB,UAAU,EAAE,EAAE,CAAC,EAAE,EAAE;CACpB,CAAC,CAAC,CAAC;AACJ,EAAE,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,gBAAgB,EAAE,SAAS,EAAE,eAAe,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;AAC9F,EAAE,CAAC,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,gBAAgB,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;AAC3E,EAAE,CAAC,IAAI,CAAC,gBAAgB,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE;IACjD,MAAM,MAAM,GAAG,MAAM,cAAc,EAAmC,CAAC;IACvE,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;AAC7C,CAAC,CAAC,CAAC;AACH,EAAE,CAAC,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE,CAAC,CAAC;IACtC,eAAe,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QAClC,WAAW,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;KACnF,CAAC,CAAC;CACJ,CAAC,CAAC,CAAC;AAEJ,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAE9D,MAAM,KAAK,GAAG;IACZ,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC;IACjC,WAAW,EAAE,CAAC,OAAO,CAAC;IACtB,WAAW,EAAE,CAAC,OAAO,CAAC;IACtB,YAAY,EAAE,CAAC,OAAO,CAAC;CACxB,CAAC;AAEF,QAAQ,CAAC,6BAA6B,EAAE,GAAG,EAAE;IAC3C,UAAU,CAAC,GAAG,EAAE;QACd,EAAE,CAAC,aAAa,EAAE,CAAC;QACnB,SAAS,CAAC,iBAAiB,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;QAC1C,WAAW,CAAC,iBAAiB,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;QACzD,QAAQ,CAAC,iBAAiB,CAAC;YACzB,KAAK,EAAE,IAAI;YACX,UAAU,EAAE,mBAAmB;YAC/B,OAAO,EAAE,GAAG;YACZ,WAAW,EAAE,gBAAgB;SAC9B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;QAChF,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAClE,MAAM,CAAC,WAAW,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE;YAC9E,IAAI,EAAE,OAAO;YACb,EAAE,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;SAChC,CAAC,CAAC;QACH,MAAM,CAAC,QAAQ,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,UAAU,CAAC,CAAC;QAC9E,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,+CAA+C,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,CAAC,UAAU,CAAC,CAAC,oBAAoB,CACrC,MAAM,CAAC,gBAAgB,CAAC;YACtB,SAAS,EAAE,OAAO;YAClB,OAAO,EAAE,OAAO;YAChB,SAAS,EAAE,UAAU;YACrB,IAAI,EAAE,OAAO;YACb,UAAU,EAAE,mBAAmB;YAC/B,OAAO,EAAE,GAAG;SACb,CAAC,CACH,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,KAAK,IAAI,EAAE;QACnF,QAAQ,CAAC,iBAAiB,CAAC;YACzB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,uBAAuB;YAC/B,WAAW,EAAE,gBAAgB;SAC9B,CAAC,CAAC;QACH,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,CAAC,UAAU,CAAC,CAAC,oBAAoB,CACrC,MAAM,CAAC,gBAAgB,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CACxE,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,mEAAmE;IACnE,EAAE,CAAC,yEAAyE,EAAE,KAAK,IAAI,EAAE;QACvF,QAAQ,CAAC,iBAAiB,CAAC;YACzB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,gBAAgB;YACxB,WAAW,EAAE,gBAAgB;SAC9B,CAAC,CAAC;QACH,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,CAAC,UAAU,CAAC,CAAC,oBAAoB,CACrC,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC,CACpD,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,KAAK,IAAI,EAAE;QACnF,QAAQ,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC;QACtD,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,CAAC,UAAU,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC;IAC7F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,WAAW,CAAC,iBAAiB,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,0EAA0E;IAC1E,0EAA0E;IAC1E,2EAA2E;IAC3E,EAAE,CAAC,uEAAuE,EAAE,KAAK,IAAI,EAAE;QACrF,QAAQ,CAAC,iBAAiB,CAAC;YACzB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,uBAAuB;YAC/B,WAAW,EAAE,gBAAgB;SAC9B,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAClE,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,sCAAsC,CAAC,CAAC;QAC/D,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QAC3C,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8EAA8E,EAAE,KAAK,IAAI,EAAE;QAC5F,QAAQ,CAAC,iBAAiB,CAAC;YACzB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,gBAAgB;YACxB,WAAW,EAAE,gBAAgB;SAC9B,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAClE,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC;QAC5C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,kCAAkC,CAAC,CAAC;QAC3D,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,KAAK,IAAI,EAAE;QACnF,QAAQ,CAAC,iBAAiB,CAAC;YACzB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,kBAAkB;YAC1B,WAAW,EAAE,gBAAgB;SAC9B,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAClE,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,0BAA0B,CAAC,CAAC;QACnD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,kCAAkC,CAAC,CAAC;QAC3D,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,QAAQ,CAAC,iBAAiB,CAAC;YACzB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,cAAc;YACtB,WAAW,EAAE,gBAAgB;SAC9B,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAClE,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC;QAC5C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,6CAA6C,CAAC,CAAC;QACtE,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,SAAS,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC;QACtC,MAAM,MAAM,CAAC,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC5E,uBAAuB,CACxB,CAAC;QACF,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8EAA8E,EAAE,KAAK,IAAI,EAAE;QAC5F,QAAQ,CAAC,qBAAqB,CAAC,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC,CAAC;QAChE,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAClE,MAAM,CAAC,WAAW,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,0BAA0B,CAAC,CAAC;QACnD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0EAA0E,EAAE,KAAK,IAAI,EAAE;QACxF,WAAW,CAAC,qBAAqB,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;QAClE,MAAM,MAAM,CAAC,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACzF,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,SAAS,CAAC,qBAAqB,CAAC;YAC9B,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;YACxB,WAAW,EAAE,EAAE;YACf,WAAW,EAAE,EAAE;YACf,YAAY,EAAE,EAAE;SACjB,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QAC/F,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,SAAS,CAAC,qBAAqB,CAAC;YAC9B,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;YACxB,WAAW,EAAE,CAAC,OAAO,CAAC;YACtB,WAAW,EAAE,CAAC,OAAO,CAAC;YACtB,YAAY,EAAE,EAAE;SACjB,CAAC,CAAC;QACH,MAAM,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,CAAC,WAAW,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;YACpF,IAAI,EAAE,OAAO;YACb,EAAE,EAAE,CAAC,OAAO,CAAC;SACd,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oFAAoF,EAAE,KAAK,IAAI,EAAE;QAClG,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,uBAAuB,CAAC,CAAC;QACjE,eAAuD,CAAC,qBAAqB,CAAC;YAC7E,WAAW,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;SAC3F,CAAC,CAAC;QACH,WAAW,CAAC,qBAAqB,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;QACzD,MAAM,MAAM,CAAC,cAAc,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC1E,uDAAuD,CACxD,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=email-send-sent-copy.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-send-sent-copy.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/email-send-sent-copy.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ const sendEmail = vi.hoisted(() => vi.fn());
3
+ const verifyAndRecordSentCopy = vi.hoisted(() => vi.fn());
4
+ const loadCredentials = vi.hoisted(() => vi.fn());
5
+ vi.mock("../lib/smtp.js", async (importOriginal) => {
6
+ const actual = await importOriginal();
7
+ return { ...actual, sendEmail };
8
+ });
9
+ vi.mock("../lib/sent-copy-note.js", () => ({ verifyAndRecordSentCopy }));
10
+ vi.mock("../lib/credentials.js", () => ({ loadCredentials }));
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ loadCredentials.mockResolvedValue({
14
+ credentials: {
15
+ config: { email: "a@example.com", agentAddress: "a@example.com" },
16
+ password: "pw",
17
+ },
18
+ });
19
+ sendEmail.mockResolvedValue({ messageId: "m1@example.com" });
20
+ });
21
+ describe("email-send sent-copy reporting", () => {
22
+ it("appends the confirmed-copy clause to a successful send", async () => {
23
+ const { emailSend } = await import("../tools/email-send.js");
24
+ verifyAndRecordSentCopy.mockResolvedValue("copy confirmed in [Gmail]/Sent Mail (UID 42)");
25
+ const out = await emailSend({
26
+ accountId: "acct1",
27
+ to: "b@example.com",
28
+ subject: "s",
29
+ body: "b",
30
+ });
31
+ expect(out).toContain("Email sent to b@example.com");
32
+ expect(out).toContain("copy confirmed in [Gmail]/Sent Mail (UID 42)");
33
+ });
34
+ it("still reports the send as successful when no copy was found", async () => {
35
+ const { emailSend } = await import("../tools/email-send.js");
36
+ verifyAndRecordSentCopy.mockResolvedValue("no copy was found in [Gmail]/Sent Mail");
37
+ const out = await emailSend({
38
+ accountId: "acct1",
39
+ to: "b@example.com",
40
+ subject: "s",
41
+ body: "b",
42
+ });
43
+ expect(out).toContain("Email sent to b@example.com");
44
+ expect(out).toContain("no copy was found in [Gmail]/Sent Mail");
45
+ expect(out).not.toMatch(/fail|error/i);
46
+ });
47
+ it("passes the account and message id through to the verifier", async () => {
48
+ const { emailSend } = await import("../tools/email-send.js");
49
+ verifyAndRecordSentCopy.mockResolvedValue("copy confirmed in S (UID 1)");
50
+ await emailSend({ accountId: "acct1", to: "b@example.com", subject: "s", body: "b" });
51
+ expect(verifyAndRecordSentCopy).toHaveBeenCalledWith(expect.objectContaining({ accountId: "acct1", messageId: "m1@example.com" }));
52
+ });
53
+ });
54
+ //# sourceMappingURL=email-send-sent-copy.test.js.map