clawmini 0.0.8 → 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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +14 -0
- package/.github/workflows/release.yml +49 -0
- package/CHANGELOG.md +36 -0
- package/README.md +5 -4
- package/dist/adapter-discord/index.d.mts.map +1 -1
- package/dist/adapter-discord/index.mjs +465 -282
- package/dist/adapter-discord/index.mjs.map +1 -1
- package/dist/adapter-google-chat/index.mjs +367 -243
- package/dist/adapter-google-chat/index.mjs.map +1 -1
- package/dist/cli/index.mjs +684 -24
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lite.mjs +43 -13
- package/dist/cli/lite.mjs.map +1 -1
- package/dist/cli/{propose-policy.mjs → manage-policies.mjs} +270 -47
- package/dist/cli/manage-policies.mjs.map +1 -0
- package/dist/cli/run-host.d.mts +1 -0
- package/dist/cli/run-host.mjs +3090 -0
- package/dist/cli/run-host.mjs.map +1 -0
- package/dist/config-CPFQIGdG.mjs +57 -0
- package/dist/config-CPFQIGdG.mjs.map +1 -0
- package/dist/config-Dvl-Pov4.mjs +76 -0
- package/dist/config-Dvl-Pov4.mjs.map +1 -0
- package/dist/daemon/index.d.mts.map +1 -1
- package/dist/daemon/index.mjs +970 -332
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/supervisor-actions-CiW56eLi.mjs +843 -0
- package/dist/supervisor-actions-CiW56eLi.mjs.map +1 -0
- package/dist/turn-log-buffer-DRgW53gl.mjs +767 -0
- package/dist/turn-log-buffer-DRgW53gl.mjs.map +1 -0
- package/dist/web/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
- package/dist/web/_app/immutable/chunks/BhRSsUCh.js +2 -0
- package/dist/web/_app/immutable/chunks/BiLeM2i1.js +1 -0
- package/{web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js → dist/web/_app/immutable/chunks/BmBj85Ll.js} +1 -1
- package/dist/web/_app/immutable/chunks/BrERcKAH.js +1 -0
- package/dist/web/_app/immutable/chunks/Bv9252RM.js +1 -0
- package/dist/web/_app/immutable/chunks/CIXNBPKi.js +1 -0
- package/dist/web/_app/immutable/chunks/DISKL3GN.js +2 -0
- package/dist/web/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
- package/dist/web/_app/immutable/chunks/DnQ3vS13.js +1 -0
- package/dist/web/_app/immutable/chunks/KsloHTKS.js +1 -0
- package/{web/.svelte-kit/output/client/_app/immutable/chunks/Ck-be5J2.js → dist/web/_app/immutable/chunks/RsHsUj-8.js} +2 -2
- package/dist/web/_app/immutable/chunks/{vDehDcuJ.js → wpfV79dV.js} +1 -1
- package/dist/web/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
- package/dist/web/_app/immutable/entry/start.Di0-Jhte.js +1 -0
- package/dist/web/_app/immutable/nodes/{0.CUGC2p-K.js → 0.DYyUA1au.js} +1 -1
- package/dist/web/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
- package/dist/web/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
- package/{web/.svelte-kit/output/client/_app/immutable/nodes/3.0arZe_Uf.js → dist/web/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
- package/dist/web/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
- package/dist/web/_app/immutable/nodes/{5.Bq2JzCEj.js → 5.DLbPVJY2.js} +1 -1
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +12 -12
- package/dist/workspace-oWmVh5mi.mjs +1001 -0
- package/dist/workspace-oWmVh5mi.mjs.map +1 -0
- package/docs/23_adapter_slash_autocomplete/development_log.md +19 -0
- package/docs/23_adapter_slash_autocomplete/notes.md +18 -0
- package/docs/23_adapter_slash_autocomplete/prd.md +46 -0
- package/docs/23_adapter_slash_autocomplete/questions.md +6 -0
- package/docs/23_adapter_slash_autocomplete/tickets.md +21 -0
- package/docs/24_subagent_job_policy_fixes/development_log.md +22 -0
- package/docs/24_subagent_job_policy_fixes/notes.md +28 -0
- package/docs/24_subagent_job_policy_fixes/prd.md +59 -0
- package/docs/24_subagent_job_policy_fixes/questions.md +3 -0
- package/docs/24_subagent_job_policy_fixes/tickets.md +49 -0
- package/docs/25_e2e_test_improvements/development_log.md +30 -0
- package/docs/25_e2e_test_improvements/notes.md +29 -0
- package/docs/25_e2e_test_improvements/prd.md +43 -0
- package/docs/25_e2e_test_improvements/questions.md +12 -0
- package/docs/25_e2e_test_improvements/tickets-2.md +22 -0
- package/docs/25_e2e_test_improvements/tickets.md +22 -0
- package/docs/25_policy_cwd/development_log.md +30 -0
- package/docs/25_policy_cwd/notes.md +28 -0
- package/docs/25_policy_cwd/prd.md +77 -0
- package/docs/25_policy_cwd/questions.md +6 -0
- package/docs/25_policy_cwd/tickets.md +77 -0
- package/docs/CLI_REFERENCE.md +3 -1
- package/docs/PHILOSOPHY.md +35 -0
- package/docs/adapter-visibility/SPEC.md +461 -0
- package/docs/adapter-visibility/SPEC_v2.md +202 -0
- package/docs/auto-update/SPEC.md +344 -0
- package/docs/backups/SPEC.md +296 -0
- package/docs/backups/clawmini.gitignore +69 -0
- package/docs/guides/assets/clawmini-avatar.png +0 -0
- package/docs/guides/backups.md +332 -0
- package/docs/guides/discord_adapter_setup.md +1 -1
- package/docs/guides/google_chat_adapter_setup.md +81 -0
- package/docs/unified-startup/SPEC.md +203 -0
- package/e2e/_helpers/test-environment.test.ts +49 -0
- package/e2e/_helpers/test-environment.ts +548 -0
- package/e2e/adapters/_google-chat-fixtures.ts +340 -0
- package/{src/cli/e2e → e2e/adapters}/adapter-discord.test.ts +22 -23
- package/e2e/adapters/adapter-google-chat-downtime.test.ts +157 -0
- package/e2e/adapters/adapter-google-chat-inbound.test.ts +697 -0
- package/e2e/adapters/adapter-google-chat-outbound.test.ts +297 -0
- package/e2e/adapters/adapter-google-chat-roundtrip.test.ts +56 -0
- package/e2e/adapters/adapter-google-chat-threads.test.ts +1078 -0
- package/e2e/agents/custom-api-env.test.ts +80 -0
- package/e2e/agents/export-lite-func.test.ts +104 -0
- package/e2e/agents/fallbacks.test.ts +124 -0
- package/e2e/agents/interrupt.test.ts +50 -0
- package/e2e/agents/no-reply-necessary.test.ts +57 -0
- package/e2e/agents/session-timeout-subagents.test.ts +76 -0
- package/e2e/agents/subagent-authorization.test.ts +246 -0
- package/e2e/agents/subagent-env.test.ts +49 -0
- package/e2e/agents/subagent-lifecycle.test.ts +782 -0
- package/e2e/agents/subagents-depth.test.ts +47 -0
- package/e2e/cli/agents.test.ts +176 -0
- package/e2e/cli/auto-update.test.ts +741 -0
- package/e2e/cli/basic.test.ts +44 -0
- package/{src/cli/e2e → e2e/cli}/export-lite.test.ts +16 -12
- package/e2e/cli/init-gitignore.test.ts +86 -0
- package/e2e/cli/init.test.ts +76 -0
- package/e2e/cli/messages.test.ts +363 -0
- package/e2e/cli/serve.test.ts +76 -0
- package/{src/cli/e2e → e2e/cli}/skills.test.ts +11 -10
- package/{src/cli/e2e → e2e/daemon}/daemon.test.ts +57 -195
- package/e2e/jobs/agent-jobs.test.ts +216 -0
- package/e2e/jobs/cron.test.ts +64 -0
- package/e2e/jobs/restart.test.ts +108 -0
- package/e2e/policies/approval-session.test.ts +69 -0
- package/e2e/policies/auto-create-policies-file.test.ts +35 -0
- package/e2e/policies/builtin-manage-policies.test.ts +184 -0
- package/e2e/policies/builtin-run-host.test.ts +180 -0
- package/e2e/policies/environment-policies.test.ts +177 -0
- package/e2e/policies/manage-policies.test.ts +566 -0
- package/e2e/policies/output-size.test.ts +98 -0
- package/e2e/policies/policies-context-cwd.test.ts +160 -0
- package/e2e/policies/relative-script-path.test.ts +60 -0
- package/e2e/policies/requests-show.test.ts +135 -0
- package/e2e/policies/requests.test.ts +208 -0
- package/e2e/policies/slash-policies.test.ts +308 -0
- package/e2e/policies/startup-cleanup.test.ts +48 -0
- package/e2e/routers/session-timeout.test.ts +106 -0
- package/e2e/routers/slash-model.test.ts +152 -0
- package/e2e/routers/slash-new.test.ts +50 -0
- package/e2e/routers/slash-restart-adapter.test.ts +96 -0
- package/e2e/routers/slash-restart.test.ts +114 -0
- package/e2e/routers/slash-shutdown.test.ts +55 -0
- package/e2e/routers/slash-stop.test.ts +232 -0
- package/e2e/routers/slash-upgrade.test.ts +88 -0
- package/{src/cli/e2e → e2e/sandbox}/environments.test.ts +14 -13
- package/eslint.config.js +6 -0
- package/napkin.md +1 -1
- package/package.json +8 -3
- package/src/adapter-discord/commands.test.ts +42 -0
- package/src/adapter-discord/commands.ts +33 -0
- package/src/adapter-discord/config.ts +12 -0
- package/src/adapter-discord/forwarder.test.ts +499 -21
- package/src/adapter-discord/forwarder.ts +343 -124
- package/src/adapter-discord/inbound-cache.test.ts +47 -0
- package/src/adapter-discord/inbound-cache.ts +37 -0
- package/src/adapter-discord/index.test.ts +67 -2
- package/src/adapter-discord/index.ts +84 -216
- package/src/adapter-discord/interactions.test.ts +54 -3
- package/src/adapter-discord/interactions.ts +97 -53
- package/src/adapter-discord/processMessage.ts +239 -0
- package/src/adapter-discord/state.ts +1 -0
- package/src/adapter-google-chat/auth.test.ts +9 -5
- package/src/adapter-google-chat/auth.ts +29 -23
- package/src/adapter-google-chat/cards.ts +7 -2
- package/src/adapter-google-chat/client.test.ts +37 -2
- package/src/adapter-google-chat/client.ts +138 -38
- package/src/adapter-google-chat/config.ts +19 -0
- package/src/adapter-google-chat/forwarder.test.ts +81 -56
- package/src/adapter-google-chat/forwarder.ts +394 -185
- package/src/adapter-google-chat/inbound-cache.test.ts +61 -0
- package/src/adapter-google-chat/inbound-cache.ts +36 -0
- package/src/adapter-google-chat/state.test.ts +1 -0
- package/src/adapter-google-chat/state.ts +9 -1
- package/src/adapter-google-chat/subscriptions.ts +8 -6
- package/src/cli/builtin-policies.ts +44 -0
- package/src/cli/commands/agents.ts +59 -5
- package/src/cli/commands/down.ts +54 -2
- package/src/cli/commands/environments.ts +8 -2
- package/src/cli/commands/init.ts +31 -0
- package/src/cli/commands/logs.ts +116 -0
- package/src/cli/commands/policies.ts +6 -4
- package/src/cli/commands/serve.test.ts +67 -0
- package/src/cli/commands/serve.ts +284 -0
- package/src/cli/commands/up.ts +122 -2
- package/src/cli/commands/web-api/agents.ts +3 -2
- package/src/cli/index.ts +4 -0
- package/src/cli/install-detection.test.ts +72 -0
- package/src/cli/install-detection.ts +48 -0
- package/src/cli/lite.ts +54 -22
- package/src/cli/manage-policies-utils.ts +104 -0
- package/src/cli/manage-policies.ts +291 -0
- package/src/cli/run-host.ts +45 -0
- package/src/cli/supervisor-actions.ts +267 -0
- package/src/cli/supervisor-control.test.ts +129 -0
- package/src/cli/supervisor-control.ts +155 -0
- package/src/cli/supervisor-pid.ts +68 -0
- package/src/cli/supervisor.ts +277 -0
- package/src/daemon/agent/agent-context.ts +11 -11
- package/src/daemon/agent/agent-session.ts +8 -1
- package/src/daemon/agent/chat-logger.test.ts +78 -9
- package/src/daemon/agent/chat-logger.ts +25 -5
- package/src/daemon/agent/turn-registry.test.ts +89 -0
- package/src/daemon/agent/turn-registry.ts +94 -0
- package/src/daemon/agent/types.ts +2 -0
- package/src/daemon/api/agent-policy-endpoints.ts +263 -0
- package/src/daemon/api/agent-router.ts +47 -126
- package/src/daemon/api/index.test.ts +1 -0
- package/src/daemon/api/policy-request.test.ts +7 -5
- package/src/daemon/api/router-utils.ts +6 -5
- package/src/daemon/api/subagent-router.ts +110 -74
- package/src/daemon/api/subagent-utils.test.ts +60 -0
- package/src/daemon/api/subagent-utils.ts +113 -87
- package/src/daemon/api/user-router.ts +34 -8
- package/src/daemon/auth.ts +1 -0
- package/src/daemon/cron.test.ts +62 -4
- package/src/daemon/cron.ts +42 -16
- package/src/daemon/events.ts +65 -0
- package/src/daemon/index.ts +24 -1
- package/src/daemon/message-interruption.test.ts +1 -0
- package/src/daemon/message-jobs.test.ts +1 -0
- package/src/daemon/message.ts +78 -14
- package/src/daemon/observation.test.ts +26 -18
- package/src/daemon/pending-replies.test.ts +112 -0
- package/src/daemon/pending-replies.ts +162 -0
- package/src/daemon/policy-request-service.ts +3 -1
- package/src/daemon/policy-utils.test.ts +66 -1
- package/src/daemon/policy-utils.ts +126 -1
- package/src/daemon/request-store.ts +31 -0
- package/src/daemon/routers/session-timeout.ts +4 -0
- package/src/daemon/routers/slash-model.test.ts +344 -0
- package/src/daemon/routers/slash-model.ts +207 -0
- package/src/daemon/routers/slash-policies.test.ts +38 -32
- package/src/daemon/routers/slash-policies.ts +84 -33
- package/src/daemon/routers/slash-restart.test.ts +69 -0
- package/src/daemon/routers/slash-restart.ts +36 -0
- package/src/daemon/routers/slash-shutdown.test.ts +50 -0
- package/src/daemon/routers/slash-shutdown.ts +28 -0
- package/src/daemon/routers/slash-upgrade.test.ts +116 -0
- package/src/daemon/routers/slash-upgrade.ts +76 -0
- package/src/daemon/routers/types.ts +7 -0
- package/src/daemon/routers.ts +16 -0
- package/src/shared/adapters/blockquote.test.ts +28 -0
- package/src/shared/adapters/blockquote.ts +20 -0
- package/src/shared/adapters/filtering.test.ts +224 -10
- package/src/shared/adapters/filtering.ts +95 -7
- package/src/shared/adapters/inbound-cache.test.ts +48 -0
- package/src/shared/adapters/inbound-cache.ts +54 -0
- package/src/shared/adapters/turn-log-buffer.ts +266 -0
- package/src/shared/adapters/turn-log.test.ts +389 -0
- package/src/shared/adapters/turn-log.ts +357 -0
- package/src/shared/agent-utils.ts +12 -5
- package/src/shared/chats.test.ts +4 -0
- package/src/shared/chats.ts +9 -0
- package/src/shared/config.ts +16 -1
- package/src/shared/lite.ts +76 -2
- package/src/shared/policies.ts +26 -0
- package/src/shared/template-manifest.ts +267 -0
- package/src/shared/utils/shell.ts +61 -0
- package/src/shared/version.ts +34 -0
- package/src/shared/workspace.test.ts +217 -0
- package/src/shared/workspace.ts +626 -48
- package/templates/environments/cladding/allowlist-domain.mjs +125 -0
- package/templates/environments/cladding/env.json +21 -1
- package/templates/environments/cladding/run-with-network.mjs +54 -0
- package/templates/environments/macos-proxy/allowlist-domain.mjs +95 -0
- package/templates/environments/macos-proxy/env.json +8 -1
- package/templates/environments/macos-proxy/proxy.mjs +0 -1
- package/templates/gemini/template.json +5 -0
- package/templates/gemini-claw/template.json +13 -0
- package/templates/skills/clawmini-requests/SKILL.md +69 -10
- package/templates/skills/run-host/SKILL.md +51 -0
- package/templates/skills/skill-creator/SKILL.md +4 -3
- package/templates/skills/skill-creator/scripts/validate.sh +52 -0
- package/tsdown.config.ts +10 -1
- package/vitest.config.ts +2 -2
- package/web/.svelte-kit/ambient.d.ts +292 -118
- package/web/.svelte-kit/generated/server/internal.js +1 -1
- package/web/.svelte-kit/output/client/.vite/manifest.json +126 -136
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BhRSsUCh.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BiLeM2i1.js +1 -0
- package/{dist/web/_app/immutable/chunks/CME08kGM.js → web/.svelte-kit/output/client/_app/immutable/chunks/BmBj85Ll.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BrERcKAH.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Bv9252RM.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CIXNBPKi.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DISKL3GN.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DnQ3vS13.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/KsloHTKS.js +1 -0
- package/{dist/web/_app/immutable/chunks/Ck-be5J2.js → web/.svelte-kit/output/client/_app/immutable/chunks/RsHsUj-8.js} +2 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{vDehDcuJ.js → wpfV79dV.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.Di0-Jhte.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.CUGC2p-K.js → 0.DYyUA1au.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
- package/{dist/web/_app/immutable/nodes/3.0arZe_Uf.js → web/.svelte-kit/output/client/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.Bq2JzCEj.js → 5.DLbPVJY2.js} +1 -1
- package/web/.svelte-kit/output/client/_app/version.json +1 -1
- package/web/.svelte-kit/output/server/.vite/manifest.json +12 -10
- package/web/.svelte-kit/output/server/chunks/Icon.js +1 -1
- package/web/.svelte-kit/output/server/chunks/client.js +1 -1
- package/web/.svelte-kit/output/server/chunks/exports.js +1 -1
- package/web/.svelte-kit/output/server/chunks/index-server.js +2 -1
- package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
- package/web/.svelte-kit/output/server/chunks/render-context.js +77 -0
- package/web/.svelte-kit/output/server/chunks/root.js +739 -788
- package/web/.svelte-kit/output/server/chunks/shared.js +234 -21
- package/web/.svelte-kit/output/server/index.js +126 -90
- package/web/.svelte-kit/output/server/manifest-full.js +1 -1
- package/web/.svelte-kit/output/server/manifest.js +1 -1
- package/web/.svelte-kit/output/server/nodes/0.js +1 -1
- package/web/.svelte-kit/output/server/nodes/1.js +1 -1
- package/web/.svelte-kit/output/server/nodes/2.js +1 -1
- package/web/.svelte-kit/output/server/nodes/3.js +1 -1
- package/web/.svelte-kit/output/server/nodes/4.js +1 -1
- package/web/.svelte-kit/output/server/nodes/5.js +1 -1
- package/web/.svelte-kit/output/server/remote-entry.js +245 -81
- package/web/.svelte-kit/tsconfig.json +4 -1
- package/dist/cli/propose-policy.mjs.map +0 -1
- package/dist/lite-CBxOT1y5.mjs +0 -241
- package/dist/lite-CBxOT1y5.mjs.map +0 -1
- package/dist/routing-D8rTxtaV.mjs +0 -245
- package/dist/routing-D8rTxtaV.mjs.map +0 -1
- package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +0 -1
- package/dist/web/_app/immutable/chunks/BmRlVmv6.js +0 -1
- package/dist/web/_app/immutable/chunks/CK9JZLaG.js +0 -2
- package/dist/web/_app/immutable/chunks/Ck3rYNON.js +0 -1
- package/dist/web/_app/immutable/chunks/D5iV40bG.js +0 -1
- package/dist/web/_app/immutable/chunks/DMtIqaiV.js +0 -2
- package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
- package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
- package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
- package/dist/web/_app/immutable/entry/app.BCSV3nrG.js +0 -2
- package/dist/web/_app/immutable/entry/start.D4eLEZUM.js +0 -1
- package/dist/web/_app/immutable/nodes/1.CGC_42IQ.js +0 -1
- package/dist/web/_app/immutable/nodes/4.ClM1bXLE.js +0 -60
- package/dist/workspace-BJmJBfKi.mjs +0 -456
- package/dist/workspace-BJmJBfKi.mjs.map +0 -1
- package/src/cli/e2e/agents.test.ts +0 -140
- package/src/cli/e2e/basic.test.ts +0 -43
- package/src/cli/e2e/cron.test.ts +0 -132
- package/src/cli/e2e/export-lite-func.test.ts +0 -206
- package/src/cli/e2e/fallbacks.test.ts +0 -175
- package/src/cli/e2e/init.test.ts +0 -77
- package/src/cli/e2e/messages.test.ts +0 -332
- package/src/cli/e2e/propose-policy.test.ts +0 -203
- package/src/cli/e2e/requests.test.ts +0 -180
- package/src/cli/e2e/session-timeout.test.ts +0 -192
- package/src/cli/e2e/slash-new.test.ts +0 -93
- package/src/cli/e2e/subagents.test.ts +0 -106
- package/src/cli/e2e/utils.ts +0 -66
- package/src/cli/propose-policy.ts +0 -91
- package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BmRlVmv6.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/D5iV40bG.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/app.BCSV3nrG.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.D4eLEZUM.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.CGC_42IQ.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.ClM1bXLE.js +0 -60
- package/web/.svelte-kit/output/server/chunks/false.js +0 -4
- /package/dist/cli/{propose-policy.d.mts → manage-policies.d.mts} +0 -0
- /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
|
+
});
|