clawmini 0.0.7 → 0.0.9

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 (367) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +14 -0
  3. package/.github/workflows/release.yml +49 -0
  4. package/CHANGELOG.md +36 -0
  5. package/README.md +5 -4
  6. package/dist/adapter-discord/index.d.mts.map +1 -1
  7. package/dist/adapter-discord/index.mjs +465 -282
  8. package/dist/adapter-discord/index.mjs.map +1 -1
  9. package/dist/adapter-google-chat/index.mjs +367 -243
  10. package/dist/adapter-google-chat/index.mjs.map +1 -1
  11. package/dist/cli/index.mjs +684 -24
  12. package/dist/cli/index.mjs.map +1 -1
  13. package/dist/cli/lite.mjs +43 -13
  14. package/dist/cli/lite.mjs.map +1 -1
  15. package/dist/cli/{propose-policy.mjs → manage-policies.mjs} +270 -47
  16. package/dist/cli/manage-policies.mjs.map +1 -0
  17. package/dist/cli/run-host.d.mts +1 -0
  18. package/dist/cli/run-host.mjs +3090 -0
  19. package/dist/cli/run-host.mjs.map +1 -0
  20. package/dist/config-CPFQIGdG.mjs +57 -0
  21. package/dist/config-CPFQIGdG.mjs.map +1 -0
  22. package/dist/config-Dvl-Pov4.mjs +76 -0
  23. package/dist/config-Dvl-Pov4.mjs.map +1 -0
  24. package/dist/daemon/index.d.mts.map +1 -1
  25. package/dist/daemon/index.mjs +970 -332
  26. package/dist/daemon/index.mjs.map +1 -1
  27. package/dist/supervisor-actions-CiW56eLi.mjs +843 -0
  28. package/dist/supervisor-actions-CiW56eLi.mjs.map +1 -0
  29. package/dist/turn-log-buffer-DRgW53gl.mjs +767 -0
  30. package/dist/turn-log-buffer-DRgW53gl.mjs.map +1 -0
  31. package/dist/web/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
  32. package/dist/web/_app/immutable/chunks/BhRSsUCh.js +2 -0
  33. package/dist/web/_app/immutable/chunks/BiLeM2i1.js +1 -0
  34. package/{web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js → dist/web/_app/immutable/chunks/BmBj85Ll.js} +1 -1
  35. package/dist/web/_app/immutable/chunks/BrERcKAH.js +1 -0
  36. package/dist/web/_app/immutable/chunks/Bv9252RM.js +1 -0
  37. package/dist/web/_app/immutable/chunks/CIXNBPKi.js +1 -0
  38. package/dist/web/_app/immutable/chunks/DISKL3GN.js +2 -0
  39. package/dist/web/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
  40. package/dist/web/_app/immutable/chunks/DnQ3vS13.js +1 -0
  41. package/dist/web/_app/immutable/chunks/KsloHTKS.js +1 -0
  42. package/{web/.svelte-kit/output/client/_app/immutable/chunks/Ck-be5J2.js → dist/web/_app/immutable/chunks/RsHsUj-8.js} +2 -2
  43. package/dist/web/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
  44. package/dist/web/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
  45. package/dist/web/_app/immutable/entry/start.Di0-Jhte.js +1 -0
  46. package/dist/web/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
  47. package/dist/web/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
  48. package/dist/web/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
  49. package/{web/.svelte-kit/output/client/_app/immutable/nodes/3.Dr0ot9sV.js → dist/web/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
  50. package/dist/web/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
  51. package/dist/web/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
  52. package/dist/web/_app/version.json +1 -1
  53. package/dist/web/index.html +12 -12
  54. package/dist/workspace-oWmVh5mi.mjs +1001 -0
  55. package/dist/workspace-oWmVh5mi.mjs.map +1 -0
  56. package/docs/23_adapter_slash_autocomplete/development_log.md +19 -0
  57. package/docs/23_adapter_slash_autocomplete/notes.md +18 -0
  58. package/docs/23_adapter_slash_autocomplete/prd.md +46 -0
  59. package/docs/23_adapter_slash_autocomplete/questions.md +6 -0
  60. package/docs/23_adapter_slash_autocomplete/tickets.md +21 -0
  61. package/docs/24_subagent_job_policy_fixes/development_log.md +22 -0
  62. package/docs/24_subagent_job_policy_fixes/notes.md +28 -0
  63. package/docs/24_subagent_job_policy_fixes/prd.md +59 -0
  64. package/docs/24_subagent_job_policy_fixes/questions.md +3 -0
  65. package/docs/24_subagent_job_policy_fixes/tickets.md +49 -0
  66. package/docs/25_e2e_test_improvements/development_log.md +30 -0
  67. package/docs/25_e2e_test_improvements/notes.md +29 -0
  68. package/docs/25_e2e_test_improvements/prd.md +43 -0
  69. package/docs/25_e2e_test_improvements/questions.md +12 -0
  70. package/docs/25_e2e_test_improvements/tickets-2.md +22 -0
  71. package/docs/25_e2e_test_improvements/tickets.md +22 -0
  72. package/docs/25_policy_cwd/development_log.md +30 -0
  73. package/docs/25_policy_cwd/notes.md +28 -0
  74. package/docs/25_policy_cwd/prd.md +77 -0
  75. package/docs/25_policy_cwd/questions.md +6 -0
  76. package/docs/25_policy_cwd/tickets.md +77 -0
  77. package/docs/CLI_REFERENCE.md +3 -1
  78. package/docs/PHILOSOPHY.md +35 -0
  79. package/docs/adapter-visibility/SPEC.md +461 -0
  80. package/docs/adapter-visibility/SPEC_v2.md +202 -0
  81. package/docs/auto-update/SPEC.md +344 -0
  82. package/docs/backups/SPEC.md +296 -0
  83. package/docs/backups/clawmini.gitignore +69 -0
  84. package/docs/guides/assets/clawmini-avatar.png +0 -0
  85. package/docs/guides/backups.md +332 -0
  86. package/docs/guides/discord_adapter_setup.md +1 -1
  87. package/docs/guides/google_chat_adapter_setup.md +81 -0
  88. package/docs/unified-startup/SPEC.md +203 -0
  89. package/e2e/_helpers/test-environment.test.ts +49 -0
  90. package/e2e/_helpers/test-environment.ts +548 -0
  91. package/e2e/adapters/_google-chat-fixtures.ts +340 -0
  92. package/{src/cli/e2e → e2e/adapters}/adapter-discord.test.ts +22 -23
  93. package/e2e/adapters/adapter-google-chat-downtime.test.ts +157 -0
  94. package/e2e/adapters/adapter-google-chat-inbound.test.ts +697 -0
  95. package/e2e/adapters/adapter-google-chat-outbound.test.ts +297 -0
  96. package/e2e/adapters/adapter-google-chat-roundtrip.test.ts +56 -0
  97. package/e2e/adapters/adapter-google-chat-threads.test.ts +1078 -0
  98. package/e2e/agents/custom-api-env.test.ts +80 -0
  99. package/e2e/agents/export-lite-func.test.ts +104 -0
  100. package/e2e/agents/fallbacks.test.ts +124 -0
  101. package/e2e/agents/interrupt.test.ts +50 -0
  102. package/e2e/agents/no-reply-necessary.test.ts +57 -0
  103. package/e2e/agents/session-timeout-subagents.test.ts +76 -0
  104. package/e2e/agents/subagent-authorization.test.ts +246 -0
  105. package/e2e/agents/subagent-env.test.ts +49 -0
  106. package/e2e/agents/subagent-lifecycle.test.ts +782 -0
  107. package/e2e/agents/subagents-depth.test.ts +47 -0
  108. package/e2e/cli/agents.test.ts +176 -0
  109. package/e2e/cli/auto-update.test.ts +741 -0
  110. package/e2e/cli/basic.test.ts +44 -0
  111. package/{src/cli/e2e → e2e/cli}/export-lite.test.ts +16 -12
  112. package/e2e/cli/init-gitignore.test.ts +86 -0
  113. package/e2e/cli/init.test.ts +76 -0
  114. package/e2e/cli/messages.test.ts +363 -0
  115. package/e2e/cli/serve.test.ts +76 -0
  116. package/{src/cli/e2e → e2e/cli}/skills.test.ts +11 -10
  117. package/{src/cli/e2e → e2e/daemon}/daemon.test.ts +57 -195
  118. package/e2e/jobs/agent-jobs.test.ts +216 -0
  119. package/e2e/jobs/cron.test.ts +64 -0
  120. package/e2e/jobs/restart.test.ts +108 -0
  121. package/e2e/policies/approval-session.test.ts +69 -0
  122. package/e2e/policies/auto-create-policies-file.test.ts +35 -0
  123. package/e2e/policies/builtin-manage-policies.test.ts +184 -0
  124. package/e2e/policies/builtin-run-host.test.ts +180 -0
  125. package/e2e/policies/environment-policies.test.ts +177 -0
  126. package/e2e/policies/manage-policies.test.ts +566 -0
  127. package/e2e/policies/output-size.test.ts +98 -0
  128. package/e2e/policies/policies-context-cwd.test.ts +160 -0
  129. package/e2e/policies/relative-script-path.test.ts +60 -0
  130. package/e2e/policies/requests-show.test.ts +135 -0
  131. package/e2e/policies/requests.test.ts +208 -0
  132. package/e2e/policies/slash-policies.test.ts +308 -0
  133. package/e2e/policies/startup-cleanup.test.ts +48 -0
  134. package/e2e/routers/session-timeout.test.ts +106 -0
  135. package/e2e/routers/slash-model.test.ts +152 -0
  136. package/e2e/routers/slash-new.test.ts +50 -0
  137. package/e2e/routers/slash-restart-adapter.test.ts +96 -0
  138. package/e2e/routers/slash-restart.test.ts +114 -0
  139. package/e2e/routers/slash-shutdown.test.ts +55 -0
  140. package/e2e/routers/slash-stop.test.ts +232 -0
  141. package/e2e/routers/slash-upgrade.test.ts +88 -0
  142. package/{src/cli/e2e → e2e/sandbox}/environments.test.ts +14 -13
  143. package/eslint.config.js +6 -0
  144. package/napkin.md +1 -1
  145. package/package.json +8 -3
  146. package/src/adapter-discord/commands.test.ts +42 -0
  147. package/src/adapter-discord/commands.ts +33 -0
  148. package/src/adapter-discord/config.ts +12 -0
  149. package/src/adapter-discord/forwarder.test.ts +499 -21
  150. package/src/adapter-discord/forwarder.ts +343 -124
  151. package/src/adapter-discord/inbound-cache.test.ts +47 -0
  152. package/src/adapter-discord/inbound-cache.ts +37 -0
  153. package/src/adapter-discord/index.test.ts +67 -2
  154. package/src/adapter-discord/index.ts +84 -216
  155. package/src/adapter-discord/interactions.test.ts +54 -3
  156. package/src/adapter-discord/interactions.ts +97 -53
  157. package/src/adapter-discord/processMessage.ts +239 -0
  158. package/src/adapter-discord/state.ts +1 -0
  159. package/src/adapter-google-chat/auth.test.ts +9 -5
  160. package/src/adapter-google-chat/auth.ts +29 -23
  161. package/src/adapter-google-chat/cards.ts +7 -2
  162. package/src/adapter-google-chat/client.test.ts +37 -2
  163. package/src/adapter-google-chat/client.ts +138 -38
  164. package/src/adapter-google-chat/config.ts +19 -0
  165. package/src/adapter-google-chat/forwarder.test.ts +81 -56
  166. package/src/adapter-google-chat/forwarder.ts +394 -185
  167. package/src/adapter-google-chat/inbound-cache.test.ts +61 -0
  168. package/src/adapter-google-chat/inbound-cache.ts +36 -0
  169. package/src/adapter-google-chat/state.test.ts +1 -0
  170. package/src/adapter-google-chat/state.ts +9 -1
  171. package/src/adapter-google-chat/subscriptions.ts +8 -6
  172. package/src/cli/builtin-policies.ts +44 -0
  173. package/src/cli/commands/agents.ts +59 -5
  174. package/src/cli/commands/down.ts +54 -2
  175. package/src/cli/commands/environments.ts +8 -2
  176. package/src/cli/commands/init.ts +31 -0
  177. package/src/cli/commands/logs.ts +116 -0
  178. package/src/cli/commands/policies.ts +6 -4
  179. package/src/cli/commands/serve.test.ts +67 -0
  180. package/src/cli/commands/serve.ts +284 -0
  181. package/src/cli/commands/up.ts +122 -2
  182. package/src/cli/commands/web-api/agents.ts +3 -2
  183. package/src/cli/index.ts +4 -0
  184. package/src/cli/install-detection.test.ts +72 -0
  185. package/src/cli/install-detection.ts +48 -0
  186. package/src/cli/lite.ts +54 -22
  187. package/src/cli/manage-policies-utils.ts +104 -0
  188. package/src/cli/manage-policies.ts +291 -0
  189. package/src/cli/run-host.ts +45 -0
  190. package/src/cli/supervisor-actions.ts +267 -0
  191. package/src/cli/supervisor-control.test.ts +129 -0
  192. package/src/cli/supervisor-control.ts +155 -0
  193. package/src/cli/supervisor-pid.ts +68 -0
  194. package/src/cli/supervisor.ts +277 -0
  195. package/src/daemon/agent/agent-context.ts +11 -11
  196. package/src/daemon/agent/agent-session.ts +8 -1
  197. package/src/daemon/agent/chat-logger.test.ts +78 -9
  198. package/src/daemon/agent/chat-logger.ts +25 -5
  199. package/src/daemon/agent/turn-registry.test.ts +89 -0
  200. package/src/daemon/agent/turn-registry.ts +94 -0
  201. package/src/daemon/agent/types.ts +2 -0
  202. package/src/daemon/api/agent-policy-endpoints.ts +263 -0
  203. package/src/daemon/api/agent-router.ts +47 -126
  204. package/src/daemon/api/index.test.ts +1 -0
  205. package/src/daemon/api/policy-request.test.ts +7 -5
  206. package/src/daemon/api/router-utils.ts +6 -5
  207. package/src/daemon/api/subagent-router.ts +110 -74
  208. package/src/daemon/api/subagent-utils.test.ts +60 -0
  209. package/src/daemon/api/subagent-utils.ts +113 -87
  210. package/src/daemon/api/user-router.ts +34 -8
  211. package/src/daemon/auth.ts +1 -0
  212. package/src/daemon/cron.test.ts +62 -4
  213. package/src/daemon/cron.ts +42 -16
  214. package/src/daemon/events.ts +65 -0
  215. package/src/daemon/index.ts +24 -1
  216. package/src/daemon/message-interruption.test.ts +1 -0
  217. package/src/daemon/message-jobs.test.ts +1 -0
  218. package/src/daemon/message.ts +78 -14
  219. package/src/daemon/observation.test.ts +26 -18
  220. package/src/daemon/pending-replies.test.ts +112 -0
  221. package/src/daemon/pending-replies.ts +162 -0
  222. package/src/daemon/policy-request-service.ts +3 -1
  223. package/src/daemon/policy-utils.test.ts +66 -1
  224. package/src/daemon/policy-utils.ts +126 -1
  225. package/src/daemon/request-store.ts +31 -0
  226. package/src/daemon/routers/session-timeout.ts +4 -0
  227. package/src/daemon/routers/slash-model.test.ts +344 -0
  228. package/src/daemon/routers/slash-model.ts +207 -0
  229. package/src/daemon/routers/slash-policies.test.ts +38 -32
  230. package/src/daemon/routers/slash-policies.ts +84 -33
  231. package/src/daemon/routers/slash-restart.test.ts +69 -0
  232. package/src/daemon/routers/slash-restart.ts +36 -0
  233. package/src/daemon/routers/slash-shutdown.test.ts +50 -0
  234. package/src/daemon/routers/slash-shutdown.ts +28 -0
  235. package/src/daemon/routers/slash-upgrade.test.ts +116 -0
  236. package/src/daemon/routers/slash-upgrade.ts +76 -0
  237. package/src/daemon/routers/types.ts +7 -0
  238. package/src/daemon/routers.ts +16 -0
  239. package/src/shared/adapters/blockquote.test.ts +28 -0
  240. package/src/shared/adapters/blockquote.ts +20 -0
  241. package/src/shared/adapters/filtering.test.ts +224 -10
  242. package/src/shared/adapters/filtering.ts +95 -7
  243. package/src/shared/adapters/inbound-cache.test.ts +48 -0
  244. package/src/shared/adapters/inbound-cache.ts +54 -0
  245. package/src/shared/adapters/turn-log-buffer.ts +266 -0
  246. package/src/shared/adapters/turn-log.test.ts +389 -0
  247. package/src/shared/adapters/turn-log.ts +357 -0
  248. package/src/shared/agent-utils.ts +12 -5
  249. package/src/shared/chats.test.ts +4 -0
  250. package/src/shared/chats.ts +9 -0
  251. package/src/shared/config.ts +16 -1
  252. package/src/shared/lite.ts +76 -2
  253. package/src/shared/policies.ts +26 -0
  254. package/src/shared/template-manifest.ts +267 -0
  255. package/src/shared/utils/shell.ts +61 -0
  256. package/src/shared/version.ts +34 -0
  257. package/src/shared/workspace.test.ts +217 -0
  258. package/src/shared/workspace.ts +626 -48
  259. package/templates/environments/cladding/allowlist-domain.mjs +125 -0
  260. package/templates/environments/cladding/env.json +21 -1
  261. package/templates/environments/cladding/run-with-network.mjs +54 -0
  262. package/templates/environments/macos-proxy/allowlist-domain.mjs +95 -0
  263. package/templates/environments/macos-proxy/env.json +8 -1
  264. package/templates/environments/macos-proxy/proxy.mjs +42 -13
  265. package/templates/gemini/template.json +5 -0
  266. package/templates/gemini-claw/template.json +13 -0
  267. package/templates/skills/clawmini-requests/SKILL.md +69 -10
  268. package/templates/skills/run-host/SKILL.md +51 -0
  269. package/templates/skills/skill-creator/SKILL.md +4 -3
  270. package/templates/skills/skill-creator/scripts/validate.sh +52 -0
  271. package/tsdown.config.ts +10 -1
  272. package/vitest.config.ts +2 -2
  273. package/web/.svelte-kit/ambient.d.ts +292 -176
  274. package/web/.svelte-kit/generated/server/internal.js +1 -1
  275. package/web/.svelte-kit/output/client/.vite/manifest.json +127 -137
  276. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
  277. package/web/.svelte-kit/output/client/_app/immutable/chunks/BhRSsUCh.js +2 -0
  278. package/web/.svelte-kit/output/client/_app/immutable/chunks/BiLeM2i1.js +1 -0
  279. package/{dist/web/_app/immutable/chunks/CME08kGM.js → web/.svelte-kit/output/client/_app/immutable/chunks/BmBj85Ll.js} +1 -1
  280. package/web/.svelte-kit/output/client/_app/immutable/chunks/BrERcKAH.js +1 -0
  281. package/web/.svelte-kit/output/client/_app/immutable/chunks/Bv9252RM.js +1 -0
  282. package/web/.svelte-kit/output/client/_app/immutable/chunks/CIXNBPKi.js +1 -0
  283. package/web/.svelte-kit/output/client/_app/immutable/chunks/DISKL3GN.js +2 -0
  284. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
  285. package/web/.svelte-kit/output/client/_app/immutable/chunks/DnQ3vS13.js +1 -0
  286. package/web/.svelte-kit/output/client/_app/immutable/chunks/KsloHTKS.js +1 -0
  287. package/{dist/web/_app/immutable/chunks/Ck-be5J2.js → web/.svelte-kit/output/client/_app/immutable/chunks/RsHsUj-8.js} +2 -2
  288. package/web/.svelte-kit/output/client/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
  289. package/web/.svelte-kit/output/client/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
  290. package/web/.svelte-kit/output/client/_app/immutable/entry/start.Di0-Jhte.js +1 -0
  291. package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
  292. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
  293. package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
  294. package/{dist/web/_app/immutable/nodes/3.Dr0ot9sV.js → web/.svelte-kit/output/client/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
  295. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
  296. package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
  297. package/web/.svelte-kit/output/client/_app/version.json +1 -1
  298. package/web/.svelte-kit/output/server/.vite/manifest.json +12 -10
  299. package/web/.svelte-kit/output/server/chunks/Icon.js +1 -1
  300. package/web/.svelte-kit/output/server/chunks/client.js +1 -1
  301. package/web/.svelte-kit/output/server/chunks/exports.js +1 -1
  302. package/web/.svelte-kit/output/server/chunks/index-server.js +2 -1
  303. package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
  304. package/web/.svelte-kit/output/server/chunks/render-context.js +77 -0
  305. package/web/.svelte-kit/output/server/chunks/root.js +739 -788
  306. package/web/.svelte-kit/output/server/chunks/shared.js +234 -21
  307. package/web/.svelte-kit/output/server/index.js +126 -90
  308. package/web/.svelte-kit/output/server/manifest-full.js +1 -1
  309. package/web/.svelte-kit/output/server/manifest.js +1 -1
  310. package/web/.svelte-kit/output/server/nodes/0.js +1 -1
  311. package/web/.svelte-kit/output/server/nodes/1.js +1 -1
  312. package/web/.svelte-kit/output/server/nodes/2.js +1 -1
  313. package/web/.svelte-kit/output/server/nodes/3.js +1 -1
  314. package/web/.svelte-kit/output/server/nodes/4.js +1 -1
  315. package/web/.svelte-kit/output/server/nodes/5.js +1 -1
  316. package/web/.svelte-kit/output/server/remote-entry.js +245 -81
  317. package/web/.svelte-kit/tsconfig.json +4 -1
  318. package/dist/cli/propose-policy.mjs.map +0 -1
  319. package/dist/lite-CBxOT1y5.mjs +0 -241
  320. package/dist/lite-CBxOT1y5.mjs.map +0 -1
  321. package/dist/routing-D8rTxtaV.mjs +0 -245
  322. package/dist/routing-D8rTxtaV.mjs.map +0 -1
  323. package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +0 -1
  324. package/dist/web/_app/immutable/chunks/BmRlVmv6.js +0 -1
  325. package/dist/web/_app/immutable/chunks/CK9JZLaG.js +0 -2
  326. package/dist/web/_app/immutable/chunks/Ck3rYNON.js +0 -1
  327. package/dist/web/_app/immutable/chunks/DMtIqaiV.js +0 -2
  328. package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
  329. package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
  330. package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
  331. package/dist/web/_app/immutable/chunks/bBmtyQMj.js +0 -1
  332. package/dist/web/_app/immutable/entry/app.CJmSwntr.js +0 -2
  333. package/dist/web/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
  334. package/dist/web/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
  335. package/dist/web/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
  336. package/dist/workspace-BJmJBfKi.mjs +0 -456
  337. package/dist/workspace-BJmJBfKi.mjs.map +0 -1
  338. package/src/cli/e2e/agents.test.ts +0 -140
  339. package/src/cli/e2e/basic.test.ts +0 -43
  340. package/src/cli/e2e/cron.test.ts +0 -132
  341. package/src/cli/e2e/export-lite-func.test.ts +0 -206
  342. package/src/cli/e2e/fallbacks.test.ts +0 -175
  343. package/src/cli/e2e/init.test.ts +0 -77
  344. package/src/cli/e2e/messages.test.ts +0 -332
  345. package/src/cli/e2e/propose-policy.test.ts +0 -203
  346. package/src/cli/e2e/requests.test.ts +0 -180
  347. package/src/cli/e2e/session-timeout.test.ts +0 -192
  348. package/src/cli/e2e/slash-new.test.ts +0 -93
  349. package/src/cli/e2e/subagents.test.ts +0 -106
  350. package/src/cli/e2e/utils.ts +0 -66
  351. package/src/cli/propose-policy.ts +0 -91
  352. package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +0 -1
  353. package/web/.svelte-kit/output/client/_app/immutable/chunks/BmRlVmv6.js +0 -1
  354. package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +0 -2
  355. package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +0 -1
  356. package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +0 -2
  357. package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
  358. package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
  359. package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
  360. package/web/.svelte-kit/output/client/_app/immutable/chunks/bBmtyQMj.js +0 -1
  361. package/web/.svelte-kit/output/client/_app/immutable/entry/app.CJmSwntr.js +0 -2
  362. package/web/.svelte-kit/output/client/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
  363. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
  364. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
  365. package/web/.svelte-kit/output/server/chunks/false.js +0 -4
  366. /package/dist/cli/{propose-policy.d.mts → manage-policies.d.mts} +0 -0
  367. /package/{src/cli/e2e → e2e/_helpers}/global-setup.ts +0 -0
@@ -0,0 +1,697 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import {
3
+ getTRPCClient,
4
+ startGoogleChatIngestion,
5
+ } from '../../src/adapter-google-chat/client.js';
6
+ import {
7
+ readGoogleChatState,
8
+ updateGoogleChatState,
9
+ } from '../../src/adapter-google-chat/state.js';
10
+ import { getSocketPath } from '../../src/shared/workspace.js';
11
+ import {
12
+ BASE_CONFIG,
13
+ makeDmMessage,
14
+ makeFakeChatApi,
15
+ makeFakeSubscription,
16
+ makePubsubMessage,
17
+ makeSpaceMessage,
18
+ useGoogleChatAdapterEnv,
19
+ } from './_google-chat-fixtures.js';
20
+
21
+ describe('Google Chat Adapter E2E — inbound (Pub/Sub → daemon)', () => {
22
+ const envRef = useGoogleChatAdapterEnv('e2e-google-chat-inbound');
23
+
24
+ it('forwards authorized MESSAGE events to the daemon and stores a user-role message', async () => {
25
+ const { env } = envRef;
26
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
27
+ const subscription = makeFakeSubscription();
28
+ const { api } = makeFakeChatApi();
29
+
30
+ await env.addChat('gc-chat');
31
+ const chat = await env.connect('gc-chat');
32
+
33
+ startGoogleChatIngestion(
34
+ BASE_CONFIG,
35
+ trpc,
36
+ {},
37
+ { subscription, chatApi: api, startDir: env.e2eDir }
38
+ );
39
+
40
+ subscription.emitMessage(
41
+ makeDmMessage({ space: 'spaces/abc', messageId: 'm1', text: 'hello from pubsub' })
42
+ );
43
+
44
+ const msg = await chat.waitForMessage(
45
+ (m) => m.role === 'user' && m.content === 'hello from pubsub'
46
+ );
47
+ expect(msg.role).toBe('user');
48
+ });
49
+
50
+ it('drops messages from unauthorized senders', async () => {
51
+ const { env } = envRef;
52
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
53
+ const subscription = makeFakeSubscription();
54
+ const { api } = makeFakeChatApi();
55
+
56
+ await env.addChat('gc-chat');
57
+ const chat = await env.connect('gc-chat');
58
+
59
+ startGoogleChatIngestion(
60
+ BASE_CONFIG,
61
+ trpc,
62
+ {},
63
+ { subscription, chatApi: api, startDir: env.e2eDir }
64
+ );
65
+
66
+ const unauthorized = makeDmMessage({
67
+ space: 'spaces/abc',
68
+ messageId: 'm-unauth',
69
+ sender: 'stranger@example.com',
70
+ text: 'I should be dropped',
71
+ });
72
+ subscription.emitMessage(unauthorized);
73
+
74
+ // Also send an authorized message so we have something to wait on.
75
+ subscription.emitMessage(
76
+ makeDmMessage({ space: 'spaces/abc', messageId: 'm-ok', text: 'I should get through' })
77
+ );
78
+
79
+ await chat.waitForMessage((m) => m.content === 'I should get through');
80
+ const dropped = chat.messageBuffer.find((m) => m.content === 'I should be dropped');
81
+ expect(dropped).toBeUndefined();
82
+ // Unauthorized message is still acked so Pub/Sub doesn't redeliver it.
83
+ await vi.waitFor(() => expect(unauthorized.ack).toHaveBeenCalled());
84
+ });
85
+
86
+ it('handles /chat routing commands, updating state and replying via chat API', async () => {
87
+ const { env } = envRef;
88
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
89
+ const subscription = makeFakeSubscription();
90
+ const { api, create } = makeFakeChatApi();
91
+
92
+ // Seed the state with an existing mapping so the routing path is exercised rather
93
+ // than the "first-ever message" auto-map branch.
94
+ await updateGoogleChatState(
95
+ { channelChatMap: { 'spaces/existing': { chatId: 'gc-chat' } } },
96
+ env.e2eDir
97
+ );
98
+ await env.addChat('gc-chat');
99
+ await env.addChat('other-chat');
100
+
101
+ startGoogleChatIngestion(
102
+ BASE_CONFIG,
103
+ trpc,
104
+ {},
105
+ { subscription, chatApi: api, startDir: env.e2eDir }
106
+ );
107
+
108
+ subscription.emitMessage(
109
+ makeSpaceMessage({ space: 'spaces/route', messageId: 'cmd', text: '/chat other-chat' })
110
+ );
111
+
112
+ await vi.waitFor(() => expect(create).toHaveBeenCalled(), { timeout: 5000 });
113
+
114
+ expect(create.mock.calls[0]![0]).toMatchObject({
115
+ parent: 'spaces/route',
116
+ });
117
+
118
+ await vi.waitFor(async () => {
119
+ const state = await readGoogleChatState(env.e2eDir);
120
+ expect(state.channelChatMap?.['spaces/route']?.chatId).toBe('other-chat');
121
+ });
122
+ });
123
+
124
+ it('sends a first-contact warning when a new space messages with a mention', async () => {
125
+ const { env } = envRef;
126
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
127
+ const subscription = makeFakeSubscription();
128
+ const { api, create } = makeFakeChatApi();
129
+
130
+ await updateGoogleChatState(
131
+ { channelChatMap: { 'spaces/known': { chatId: 'gc-chat' } } },
132
+ env.e2eDir
133
+ );
134
+ await env.addChat('gc-chat');
135
+
136
+ startGoogleChatIngestion(
137
+ BASE_CONFIG,
138
+ trpc,
139
+ {},
140
+ { subscription, chatApi: api, startDir: env.e2eDir }
141
+ );
142
+
143
+ subscription.emitMessage(
144
+ makeSpaceMessage({
145
+ space: 'spaces/unmapped',
146
+ messageId: 'first',
147
+ text: '@bot hello',
148
+ // First-contact path only checks for the presence of a USER_MENTION
149
+ // annotation, so use the simple form rather than the bot-targeted one.
150
+ annotations: [{ type: 'USER_MENTION' }],
151
+ })
152
+ );
153
+
154
+ await vi.waitFor(() => expect(create).toHaveBeenCalled(), { timeout: 5000 });
155
+
156
+ const call = create.mock.calls[0]![0];
157
+ expect(call.parent).toBe('spaces/unmapped');
158
+ expect(call.requestBody.text).toContain('not currently mapped');
159
+ });
160
+
161
+ it('parses workspace-events payloads (ce-type attribute) into a forwarded message', async () => {
162
+ const { env } = envRef;
163
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
164
+ const subscription = makeFakeSubscription();
165
+ const { api } = makeFakeChatApi();
166
+
167
+ await updateGoogleChatState(
168
+ { channelChatMap: { 'spaces/wsp': { chatId: 'gc-chat' } } },
169
+ env.e2eDir
170
+ );
171
+ await env.addChat('gc-chat');
172
+ const chat = await env.connect('gc-chat');
173
+
174
+ startGoogleChatIngestion(
175
+ BASE_CONFIG,
176
+ trpc,
177
+ {},
178
+ { subscription, chatApi: api, startDir: env.e2eDir }
179
+ );
180
+
181
+ // Workspace-events envelope: no `type` field; payload is the message object itself,
182
+ // and the `ce-type` Pub/Sub attribute tells us to treat it as a MESSAGE.
183
+ subscription.emitMessage(
184
+ makePubsubMessage(
185
+ {
186
+ name: 'spaces/wsp/messages/ws1',
187
+ sender: { email: 'user@example.com', type: 'USER' },
188
+ space: { name: 'spaces/wsp', type: 'DIRECT_MESSAGE', singleUserBotDm: true },
189
+ text: 'workspace events payload',
190
+ },
191
+ { 'ce-type': 'google.workspace.chat.message.v1.created' }
192
+ )
193
+ );
194
+
195
+ const msg = await chat.waitForMessage(
196
+ (m) => m.role === 'user' && m.content === 'workspace events payload'
197
+ );
198
+ expect(msg.role).toBe('user');
199
+ });
200
+
201
+ it('auto-maps the first-ever message to config.chatId and forwards it', async () => {
202
+ const { env } = envRef;
203
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
204
+ const subscription = makeFakeSubscription();
205
+ const { api } = makeFakeChatApi();
206
+
207
+ // No seeding — afterEach already reset state, so any incoming message
208
+ // should trigger the "first-ever message" auto-map branch.
209
+ await env.addChat('gc-chat');
210
+ const chat = await env.connect('gc-chat');
211
+
212
+ startGoogleChatIngestion(
213
+ BASE_CONFIG,
214
+ trpc,
215
+ {},
216
+ { subscription, chatApi: api, startDir: env.e2eDir }
217
+ );
218
+
219
+ subscription.emitMessage(
220
+ makeSpaceMessage({
221
+ space: 'spaces/first',
222
+ messageId: 'first1',
223
+ sender: 'user@example.com',
224
+ text: 'first contact body',
225
+ })
226
+ );
227
+
228
+ const msg = await chat.waitForMessage(
229
+ (m) => m.role === 'user' && m.content === 'first contact body'
230
+ );
231
+ expect(msg.role).toBe('user');
232
+
233
+ // The space should now be mapped to the default chat id from config.
234
+ await vi.waitFor(async () => {
235
+ const state = await readGoogleChatState(env.e2eDir);
236
+ expect(state.channelChatMap?.['spaces/first']?.chatId).toBe('gc-chat');
237
+ });
238
+ });
239
+
240
+ it('drops non-mention messages in a non-DM space when requireMention is true', async () => {
241
+ const { env } = envRef;
242
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
243
+ const subscription = makeFakeSubscription();
244
+ const { api } = makeFakeChatApi();
245
+
246
+ await updateGoogleChatState(
247
+ { channelChatMap: { 'spaces/rm': { chatId: 'gc-chat' } } },
248
+ env.e2eDir
249
+ );
250
+ await env.addChat('gc-chat');
251
+ const chat = await env.connect('gc-chat');
252
+
253
+ startGoogleChatIngestion(
254
+ { ...BASE_CONFIG, requireMention: true },
255
+ trpc,
256
+ {},
257
+ { subscription, chatApi: api, startDir: env.e2eDir }
258
+ );
259
+
260
+ // Non-mention message in a mapped SPACE (not a DM) — should be ack'd and dropped.
261
+ const unmentioned = makeSpaceMessage({
262
+ space: 'spaces/rm',
263
+ messageId: 'nomention',
264
+ sender: 'user@example.com',
265
+ text: 'plain channel chatter',
266
+ });
267
+ subscription.emitMessage(unmentioned);
268
+
269
+ // A mention of the bot — should flow through to the daemon.
270
+ subscription.emitMessage(
271
+ makeSpaceMessage({
272
+ space: 'spaces/rm',
273
+ messageId: 'withmention',
274
+ sender: 'user@example.com',
275
+ text: '@bot help me',
276
+ mention: true,
277
+ })
278
+ );
279
+
280
+ await chat.waitForMessage((m) => m.content === '@bot help me');
281
+ const dropped = chat.messageBuffer.find((m) => m.content === 'plain channel chatter');
282
+ expect(dropped).toBeUndefined();
283
+ await vi.waitFor(() => expect(unmentioned.ack).toHaveBeenCalled());
284
+ });
285
+
286
+ it('issues a workspace-events subscription on ADDED_TO_SPACE', async () => {
287
+ const { env } = envRef;
288
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
289
+ const subscription = makeFakeSubscription();
290
+ const { api } = makeFakeChatApi();
291
+
292
+ const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue(
293
+ new Response(
294
+ JSON.stringify({ name: 'subscriptions/abc', expireTime: '2026-01-01T00:00:00Z' }),
295
+ { status: 200 }
296
+ )
297
+ );
298
+ const originalFetch = globalThis.fetch;
299
+ globalThis.fetch = fetchMock;
300
+
301
+ try {
302
+ startGoogleChatIngestion(
303
+ { ...BASE_CONFIG, oauthClientId: 'id', oauthClientSecret: 'secret' },
304
+ trpc,
305
+ {},
306
+ { subscription, chatApi: api, startDir: env.e2eDir }
307
+ );
308
+
309
+ // Force the user-auth client cache to look already-authed by pre-seeding oauthTokens
310
+ // in state so getUserAuthClient doesn't attempt the interactive OAuth flow.
311
+ await updateGoogleChatState(
312
+ {
313
+ oauthTokens: {
314
+ access_token: 'fake',
315
+ refresh_token: 'fake',
316
+ expiry_date: Date.now() + 1_000_000,
317
+ },
318
+ },
319
+ env.e2eDir
320
+ );
321
+
322
+ subscription.emitMessage(
323
+ makePubsubMessage({
324
+ type: 'ADDED_TO_SPACE',
325
+ space: { name: 'spaces/added', type: 'SPACE' },
326
+ user: { email: 'user@example.com' },
327
+ })
328
+ );
329
+
330
+ await vi.waitFor(() => expect(fetchMock).toHaveBeenCalled(), { timeout: 5000 });
331
+
332
+ const [url, init] = fetchMock.mock.calls[0]! as [string, RequestInit];
333
+ expect(url).toBe('https://workspaceevents.googleapis.com/v1/subscriptions');
334
+ expect(init.method).toBe('POST');
335
+
336
+ await vi.waitFor(async () => {
337
+ const state = await readGoogleChatState(env.e2eDir);
338
+ expect(state.channelChatMap?.['spaces/added']?.subscriptionId).toBe('subscriptions/abc');
339
+ });
340
+ } finally {
341
+ globalThis.fetch = originalFetch;
342
+ }
343
+ });
344
+
345
+ it('tears down the workspace-events subscription on REMOVED_FROM_SPACE', async () => {
346
+ const { env } = envRef;
347
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
348
+ const subscription = makeFakeSubscription();
349
+ const { api } = makeFakeChatApi();
350
+
351
+ await updateGoogleChatState(
352
+ {
353
+ channelChatMap: {
354
+ 'spaces/rm-done': {
355
+ chatId: 'gc-chat',
356
+ subscriptionId: 'subscriptions/rm-done',
357
+ expirationDate: '2099-01-01T00:00:00Z',
358
+ },
359
+ },
360
+ oauthTokens: {
361
+ access_token: 'fake',
362
+ refresh_token: 'fake',
363
+ expiry_date: Date.now() + 1_000_000,
364
+ },
365
+ },
366
+ env.e2eDir
367
+ );
368
+ await env.addChat('gc-chat');
369
+
370
+ const fetchMock = vi
371
+ .fn<typeof globalThis.fetch>()
372
+ .mockResolvedValue(new Response(null, { status: 200 }));
373
+ const originalFetch = globalThis.fetch;
374
+ globalThis.fetch = fetchMock;
375
+
376
+ try {
377
+ startGoogleChatIngestion(
378
+ { ...BASE_CONFIG, oauthClientId: 'id', oauthClientSecret: 'secret' },
379
+ trpc,
380
+ {},
381
+ { subscription, chatApi: api, startDir: env.e2eDir }
382
+ );
383
+
384
+ subscription.emitMessage(
385
+ makePubsubMessage({
386
+ type: 'REMOVED_FROM_SPACE',
387
+ space: { name: 'spaces/rm-done', type: 'SPACE' },
388
+ user: { email: 'user@example.com' },
389
+ })
390
+ );
391
+
392
+ await vi.waitFor(() => {
393
+ const deleteCall = fetchMock.mock.calls.find(
394
+ ([url, init]) =>
395
+ typeof url === 'string' &&
396
+ url.endsWith('/v1/subscriptions/rm-done') &&
397
+ (init as RequestInit | undefined)?.method === 'DELETE'
398
+ );
399
+ expect(deleteCall).toBeDefined();
400
+ }, { timeout: 5000 });
401
+
402
+ // The subscription fields should be stripped, but chatId preserved
403
+ // because entry.chatId was set.
404
+ await vi.waitFor(async () => {
405
+ const state = await readGoogleChatState(env.e2eDir);
406
+ const entry = state.channelChatMap?.['spaces/rm-done'];
407
+ expect(entry?.chatId).toBe('gc-chat');
408
+ expect(entry?.subscriptionId).toBeUndefined();
409
+ expect(entry?.expirationDate).toBeUndefined();
410
+ });
411
+ } finally {
412
+ globalThis.fetch = originalFetch;
413
+ }
414
+ });
415
+
416
+ it('handles CARD_CLICKED by stripping buttons and forwarding /approve to the daemon', async () => {
417
+ const { env } = envRef;
418
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
419
+ const subscription = makeFakeSubscription();
420
+ const { api, update } = makeFakeChatApi();
421
+
422
+ await updateGoogleChatState(
423
+ { channelChatMap: { 'spaces/cc': { chatId: 'gc-chat' } } },
424
+ env.e2eDir
425
+ );
426
+ await env.addChat('gc-chat');
427
+ const chat = await env.connect('gc-chat');
428
+
429
+ startGoogleChatIngestion(
430
+ BASE_CONFIG,
431
+ trpc,
432
+ {},
433
+ { subscription, chatApi: api, startDir: env.e2eDir }
434
+ );
435
+
436
+ subscription.emitMessage(
437
+ makePubsubMessage({
438
+ type: 'CARD_CLICKED',
439
+ space: { name: 'spaces/cc', type: 'DIRECT_MESSAGE', singleUserBotDm: true },
440
+ user: { email: 'user@example.com' },
441
+ message: {
442
+ name: 'spaces/cc/messages/card-1',
443
+ sender: { type: 'BOT' },
444
+ cardsV2: [
445
+ {
446
+ cardId: 'c1',
447
+ card: {
448
+ header: { title: 'Policy', subtitle: 'Pending' },
449
+ sections: [
450
+ {
451
+ widgets: [
452
+ { textParagraph: { text: 'please approve' } },
453
+ { buttonList: { buttons: [{ text: 'Approve' }, { text: 'Reject' }] } },
454
+ ],
455
+ },
456
+ ],
457
+ },
458
+ },
459
+ ],
460
+ },
461
+ action: {
462
+ actionMethodName: 'approve',
463
+ parameters: [{ key: 'policyId', value: 'pol-cc-1' }],
464
+ },
465
+ })
466
+ );
467
+
468
+ // Card update strips buttons and updates the subtitle to 'Policy Approved'.
469
+ await vi.waitFor(() => expect(update).toHaveBeenCalled(), { timeout: 5000 });
470
+ const updateCall = update.mock.calls[0]![0];
471
+ expect(updateCall.name).toBe('spaces/cc/messages/card-1');
472
+ expect(updateCall.updateMask).toBe('cardsV2');
473
+ const updatedCard = updateCall.requestBody.cardsV2![0] as {
474
+ card: {
475
+ header: { subtitle: string };
476
+ sections: { widgets: Array<Record<string, unknown>> }[];
477
+ };
478
+ };
479
+ expect(updatedCard.card.header.subtitle).toBe('Policy Approved');
480
+ for (const section of updatedCard.card.sections) {
481
+ for (const widget of section.widgets) {
482
+ expect(widget).not.toHaveProperty('buttonList');
483
+ }
484
+ }
485
+
486
+ // The daemon should receive a user message with the slash command.
487
+ await chat.waitForMessage(
488
+ (m) => m.role === 'user' && m.content === '/approve pol-cc-1'
489
+ );
490
+ });
491
+
492
+ it('handles /agent by creating a new chat and mapping the space to it', async () => {
493
+ const { env } = envRef;
494
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
495
+ const subscription = makeFakeSubscription();
496
+ const { api, create } = makeFakeChatApi();
497
+
498
+ await updateGoogleChatState(
499
+ { channelChatMap: { 'spaces/seed': { chatId: 'gc-chat' } } },
500
+ env.e2eDir
501
+ );
502
+ await env.addChat('gc-chat');
503
+ await env.addAgent('router-agent');
504
+
505
+ startGoogleChatIngestion(
506
+ BASE_CONFIG,
507
+ trpc,
508
+ {},
509
+ { subscription, chatApi: api, startDir: env.e2eDir }
510
+ );
511
+
512
+ subscription.emitMessage(
513
+ makeSpaceMessage({
514
+ space: 'spaces/agent-route',
515
+ messageId: 'cmd',
516
+ text: '/agent router-agent',
517
+ })
518
+ );
519
+
520
+ await vi.waitFor(() => expect(create).toHaveBeenCalled(), { timeout: 5000 });
521
+ const reply = create.mock.calls[0]![0];
522
+ expect(reply.parent).toBe('spaces/agent-route');
523
+ expect(reply.requestBody.text).toMatch(/Successfully created new chat/);
524
+
525
+ await vi.waitFor(async () => {
526
+ const state = await readGoogleChatState(env.e2eDir);
527
+ const newChatId = state.channelChatMap?.['spaces/agent-route']?.chatId;
528
+ expect(newChatId).toMatch(/^router-agent-google-chat/);
529
+ });
530
+ });
531
+
532
+ it('forwards quote-replies with an attribution line and the quoted message as a blockquote', async () => {
533
+ const { env } = envRef;
534
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
535
+ const subscription = makeFakeSubscription();
536
+ const { api } = makeFakeChatApi();
537
+
538
+ await updateGoogleChatState(
539
+ { channelChatMap: { 'spaces/quoted': { chatId: 'gc-chat' } } },
540
+ env.e2eDir
541
+ );
542
+ await env.addChat('gc-chat');
543
+ const chat = await env.connect('gc-chat');
544
+
545
+ startGoogleChatIngestion(
546
+ BASE_CONFIG,
547
+ trpc,
548
+ {},
549
+ { subscription, chatApi: api, startDir: env.e2eDir }
550
+ );
551
+
552
+ subscription.emitMessage(
553
+ makeDmMessage({
554
+ space: 'spaces/quoted',
555
+ messageId: 'reply-1',
556
+ text: "Yes, I'm in!",
557
+ quotedMessageMetadata: {
558
+ name: 'spaces/quoted/messages/orig-1',
559
+ quotedMessageSnapshot: {
560
+ text: 'Would anyone like to get dinner Sunday?\nOr maybe lunch?',
561
+ sender: { email: 'other@example.com', type: 'HUMAN' },
562
+ },
563
+ },
564
+ })
565
+ );
566
+
567
+ const msg = await chat.waitForMessage(
568
+ (m) => m.role === 'user' && m.content.includes("Yes, I'm in!")
569
+ );
570
+ expect(msg.content).toBe(
571
+ "> **other@example.com said:**\n> Would anyone like to get dinner Sunday?\n> Or maybe lunch?\n\nYes, I'm in!"
572
+ );
573
+ });
574
+
575
+ it('labels quoted bot messages as "Assistant" and leaves out attribution for authorized users', async () => {
576
+ const { env } = envRef;
577
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
578
+ const subscription = makeFakeSubscription();
579
+ const { api } = makeFakeChatApi();
580
+
581
+ await updateGoogleChatState(
582
+ { channelChatMap: { 'spaces/quoted2': { chatId: 'gc-chat' } } },
583
+ env.e2eDir
584
+ );
585
+ await env.addChat('gc-chat');
586
+ const chat = await env.connect('gc-chat');
587
+
588
+ startGoogleChatIngestion(
589
+ BASE_CONFIG,
590
+ trpc,
591
+ {},
592
+ { subscription, chatApi: api, startDir: env.e2eDir }
593
+ );
594
+
595
+ subscription.emitMessage(
596
+ makeDmMessage({
597
+ space: 'spaces/quoted2',
598
+ messageId: 'reply-bot',
599
+ text: 'thanks',
600
+ quotedMessageMetadata: {
601
+ name: 'spaces/quoted2/messages/bot-1',
602
+ quotedMessageSnapshot: { text: 'Done.', sender: { type: 'BOT' } },
603
+ },
604
+ })
605
+ );
606
+ const botReply = await chat.waitForMessage(
607
+ (m) => m.role === 'user' && m.content.includes('thanks')
608
+ );
609
+ expect(botReply.content).toBe('> **Assistant said:**\n> Done.\n\nthanks');
610
+
611
+ subscription.emitMessage(
612
+ makeDmMessage({
613
+ space: 'spaces/quoted2',
614
+ messageId: 'reply-self',
615
+ text: 'still relevant',
616
+ quotedMessageMetadata: {
617
+ name: 'spaces/quoted2/messages/self-1',
618
+ quotedMessageSnapshot: {
619
+ text: 'earlier note',
620
+ sender: { email: 'user@example.com', type: 'HUMAN' },
621
+ },
622
+ },
623
+ })
624
+ );
625
+ const selfReply = await chat.waitForMessage(
626
+ (m) => m.role === 'user' && m.content.includes('still relevant')
627
+ );
628
+ expect(selfReply.content).toBe('> earlier note\n\nstill relevant');
629
+
630
+ subscription.emitMessage(
631
+ makeDmMessage({
632
+ space: 'spaces/quoted2',
633
+ messageId: 'reply-fallback',
634
+ text: 'wow',
635
+ quotedMessageMetadata: {
636
+ name: 'spaces/quoted2/messages/fallback-1',
637
+ quotedMessageSnapshot: {
638
+ text: 'no email here',
639
+ sender: { displayName: 'John Doe', name: 'users/1234', type: 'HUMAN' },
640
+ },
641
+ },
642
+ })
643
+ );
644
+ const fallbackReply = await chat.waitForMessage(
645
+ (m) => m.role === 'user' && m.content.includes('wow')
646
+ );
647
+ expect(fallbackReply.content).toBe('> **John Doe said:**\n> no email here\n\nwow');
648
+ });
649
+
650
+ it('downloads attachments and forwards them with the message to the daemon', async () => {
651
+ const { env } = envRef;
652
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
653
+ const subscription = makeFakeSubscription();
654
+ const { api } = makeFakeChatApi();
655
+
656
+ await updateGoogleChatState(
657
+ { channelChatMap: { 'spaces/att': { chatId: 'gc-chat' } } },
658
+ env.e2eDir
659
+ );
660
+ await env.addChat('gc-chat');
661
+ const chat = await env.connect('gc-chat');
662
+
663
+ const fakePayload = Buffer.from('hello attachment payload');
664
+ const downloadAttachment = vi.fn(async () => fakePayload);
665
+
666
+ startGoogleChatIngestion(
667
+ BASE_CONFIG,
668
+ trpc,
669
+ {},
670
+ { subscription, chatApi: api, startDir: env.e2eDir, downloadAttachment }
671
+ );
672
+
673
+ subscription.emitMessage(
674
+ makeDmMessage({
675
+ space: 'spaces/att',
676
+ messageId: 'att1',
677
+ text: 'with attachment',
678
+ attachment: [
679
+ { contentName: 'note.txt', attachmentDataRef: { resourceName: 'media/note' } },
680
+ ],
681
+ })
682
+ );
683
+
684
+ const msg = await chat.waitForMessage(
685
+ (m) =>
686
+ m.role === 'user' &&
687
+ m.content.startsWith('with attachment') &&
688
+ m.content.includes('note.txt')
689
+ );
690
+ expect(downloadAttachment).toHaveBeenCalledWith('media/note', undefined);
691
+
692
+ // The daemon relocates uploaded files into the agent's files dir and
693
+ // suffixes the message content with an "Attached files:" block referencing
694
+ // the relative path.
695
+ expect(msg.content).toMatch(/Attached files:/);
696
+ });
697
+ });