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,1078 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
getTRPCClient,
|
|
4
|
+
startGoogleChatIngestion,
|
|
5
|
+
} from '../../src/adapter-google-chat/client.js';
|
|
6
|
+
import { updateGoogleChatState } from '../../src/adapter-google-chat/state.js';
|
|
7
|
+
import { getSocketPath } from '../../src/shared/workspace.js';
|
|
8
|
+
import {
|
|
9
|
+
BASE_CONFIG,
|
|
10
|
+
makeDmMessage,
|
|
11
|
+
makeFakeChatApi,
|
|
12
|
+
makeFakeSubscription,
|
|
13
|
+
makePubsubMessage,
|
|
14
|
+
runForwarder,
|
|
15
|
+
useGoogleChatAdapterEnv,
|
|
16
|
+
} from './_google-chat-fixtures.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* E2E tests for the threaded activity log. We exercise the full inbound path
|
|
20
|
+
* (pubsub → client → daemon → forwarder) so the GChat `message.name` gets
|
|
21
|
+
* correlated to the daemon turn via `externalRef`, just like in production.
|
|
22
|
+
*
|
|
23
|
+
* `command` messages are dropped from the turn log, so a plain-echo agent
|
|
24
|
+
* wouldn't produce any thread-log content. These tests drive the `debug-agent`
|
|
25
|
+
* with a `clawmini-lite.js subagents spawn` inbound — the emitted
|
|
26
|
+
* `subagent_status` events route to `thread-log` and give each turn a real
|
|
27
|
+
* entry to anchor on.
|
|
28
|
+
*/
|
|
29
|
+
const SPAWN_COMMAND = (id: string) =>
|
|
30
|
+
`clawmini-lite.js subagents spawn --id ${id} --async "echo x"`;
|
|
31
|
+
function makeThreadedMessage(opts: {
|
|
32
|
+
space: string;
|
|
33
|
+
messageId: string;
|
|
34
|
+
threadName: string;
|
|
35
|
+
text: string;
|
|
36
|
+
}) {
|
|
37
|
+
return makePubsubMessage({
|
|
38
|
+
type: 'MESSAGE',
|
|
39
|
+
space: { name: opts.space, type: 'SPACE' },
|
|
40
|
+
user: { email: 'user@example.com' },
|
|
41
|
+
message: {
|
|
42
|
+
name: `${opts.space}/messages/${opts.messageId}`,
|
|
43
|
+
thread: { name: opts.threadName },
|
|
44
|
+
text: opts.text,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('Google Chat Adapter E2E — threaded activity log', () => {
|
|
50
|
+
const envRef = useGoogleChatAdapterEnv('e2e-google-chat-threads', { subagents: true });
|
|
51
|
+
|
|
52
|
+
it('opens a thread anchored on the user thread and edits the log on subsequent events', async () => {
|
|
53
|
+
const { env } = envRef;
|
|
54
|
+
const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
|
|
55
|
+
const subscription = makeFakeSubscription();
|
|
56
|
+
const { api, create } = makeFakeChatApi();
|
|
57
|
+
|
|
58
|
+
create.mockImplementation(
|
|
59
|
+
async () => ({ data: { name: 'spaces/thr/messages/log-1' } }) as unknown as object
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
await updateGoogleChatState(
|
|
63
|
+
{ channelChatMap: { 'spaces/thr': { chatId: 'gc-threads' } } },
|
|
64
|
+
env.e2eDir
|
|
65
|
+
);
|
|
66
|
+
await env.addChat('gc-threads', 'debug-agent');
|
|
67
|
+
|
|
68
|
+
startGoogleChatIngestion(
|
|
69
|
+
BASE_CONFIG,
|
|
70
|
+
trpc,
|
|
71
|
+
{},
|
|
72
|
+
{ subscription, chatApi: api, startDir: env.e2eDir }
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const config = { ...BASE_CONFIG, chatId: 'gc-threads' };
|
|
76
|
+
|
|
77
|
+
await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
|
|
78
|
+
subscription.emitMessage(
|
|
79
|
+
makeThreadedMessage({
|
|
80
|
+
space: 'spaces/thr',
|
|
81
|
+
messageId: 'u1',
|
|
82
|
+
threadName: 'spaces/thr/threads/t1',
|
|
83
|
+
text: SPAWN_COMMAND('t1-sub'),
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Main agent's reply lands at top-level; the subagent's status events
|
|
88
|
+
// land in the thread-log anchored on t1.
|
|
89
|
+
await vi.waitFor(
|
|
90
|
+
() => {
|
|
91
|
+
const reply = create.mock.calls.find(
|
|
92
|
+
([p]) =>
|
|
93
|
+
typeof p.requestBody.text === 'string' &&
|
|
94
|
+
p.requestBody.text.includes('Subagent spawned successfully with ID: t1-sub') &&
|
|
95
|
+
!('thread' in p.requestBody)
|
|
96
|
+
);
|
|
97
|
+
expect(reply).toBeDefined();
|
|
98
|
+
},
|
|
99
|
+
{ timeout: 15000 }
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
await vi.waitFor(
|
|
103
|
+
() => {
|
|
104
|
+
const threaded = create.mock.calls.find(
|
|
105
|
+
([p]) =>
|
|
106
|
+
(p.requestBody as { thread?: { name?: string } }).thread?.name ===
|
|
107
|
+
'spaces/thr/threads/t1'
|
|
108
|
+
);
|
|
109
|
+
expect(threaded).toBeDefined();
|
|
110
|
+
},
|
|
111
|
+
{ timeout: 15000 }
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const threaded = create.mock.calls.find(
|
|
115
|
+
([p]) =>
|
|
116
|
+
(p.requestBody as { thread?: { name?: string } }).thread?.name ===
|
|
117
|
+
'spaces/thr/threads/t1'
|
|
118
|
+
)![0];
|
|
119
|
+
expect(threaded.parent).toBe('spaces/thr');
|
|
120
|
+
expect(
|
|
121
|
+
(threaded as unknown as { messageReplyOption?: string }).messageReplyOption
|
|
122
|
+
).toBe('REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD');
|
|
123
|
+
});
|
|
124
|
+
}, 60000);
|
|
125
|
+
|
|
126
|
+
it('routes the final agent reply to top-level, not the thread', async () => {
|
|
127
|
+
const { env } = envRef;
|
|
128
|
+
const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
|
|
129
|
+
const subscription = makeFakeSubscription();
|
|
130
|
+
const { api, create } = makeFakeChatApi();
|
|
131
|
+
|
|
132
|
+
create.mockImplementation(
|
|
133
|
+
async () => ({ data: { name: 'spaces/thr2/messages/log-1' } }) as unknown as object
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await updateGoogleChatState(
|
|
137
|
+
{ channelChatMap: { 'spaces/thr2': { chatId: 'gc-threads-2' } } },
|
|
138
|
+
env.e2eDir
|
|
139
|
+
);
|
|
140
|
+
await env.addChat('gc-threads-2');
|
|
141
|
+
|
|
142
|
+
startGoogleChatIngestion(
|
|
143
|
+
BASE_CONFIG,
|
|
144
|
+
trpc,
|
|
145
|
+
{},
|
|
146
|
+
{ subscription, chatApi: api, startDir: env.e2eDir }
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const config = { ...BASE_CONFIG, chatId: 'gc-threads-2' };
|
|
150
|
+
|
|
151
|
+
await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
|
|
152
|
+
subscription.emitMessage(
|
|
153
|
+
makeThreadedMessage({
|
|
154
|
+
space: 'spaces/thr2',
|
|
155
|
+
messageId: 'u1',
|
|
156
|
+
threadName: 'spaces/thr2/threads/t2',
|
|
157
|
+
text: 'final reply payload',
|
|
158
|
+
})
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
await vi.waitFor(
|
|
162
|
+
() => {
|
|
163
|
+
const reply = create.mock.calls.find(
|
|
164
|
+
([p]) =>
|
|
165
|
+
p.requestBody.text === 'final reply payload' && !('thread' in p.requestBody)
|
|
166
|
+
);
|
|
167
|
+
expect(reply).toBeDefined();
|
|
168
|
+
},
|
|
169
|
+
{ timeout: 15000 }
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
}, 45000);
|
|
173
|
+
|
|
174
|
+
it('anchors the activity log on the triggering message, not an earlier slash command', async () => {
|
|
175
|
+
const { env } = envRef;
|
|
176
|
+
const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
|
|
177
|
+
const subscription = makeFakeSubscription();
|
|
178
|
+
const { api, create } = makeFakeChatApi();
|
|
179
|
+
|
|
180
|
+
create.mockImplementation(
|
|
181
|
+
async () => ({ data: { name: 'spaces/newcmd/messages/log-1' } }) as unknown as object
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
await updateGoogleChatState(
|
|
185
|
+
{ channelChatMap: { 'spaces/newcmd': { chatId: 'gc-newcmd' } } },
|
|
186
|
+
env.e2eDir
|
|
187
|
+
);
|
|
188
|
+
await env.addChat('gc-newcmd');
|
|
189
|
+
|
|
190
|
+
// Register /new as a router so that a bare `/new` message does not spawn
|
|
191
|
+
// an agent turn — mirrors real deployments where /new resets the session
|
|
192
|
+
// and returns an automatic reply with no agent work. The chat's agent is
|
|
193
|
+
// `debug-agent` so the real turn can spawn a subagent (producing the
|
|
194
|
+
// subagent_status events that actually anchor the thread log).
|
|
195
|
+
env.writeChatSettings('gc-newcmd', {
|
|
196
|
+
routers: ['@clawmini/slash-new'],
|
|
197
|
+
defaultAgent: 'debug-agent',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
startGoogleChatIngestion(
|
|
201
|
+
BASE_CONFIG,
|
|
202
|
+
trpc,
|
|
203
|
+
{},
|
|
204
|
+
{ subscription, chatApi: api, startDir: env.e2eDir }
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const config = { ...BASE_CONFIG, chatId: 'gc-newcmd' };
|
|
208
|
+
|
|
209
|
+
await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
|
|
210
|
+
// First inbound: `/new`. Does not trigger a turn.
|
|
211
|
+
subscription.emitMessage(
|
|
212
|
+
makeThreadedMessage({
|
|
213
|
+
space: 'spaces/newcmd',
|
|
214
|
+
messageId: 'slash',
|
|
215
|
+
threadName: 'spaces/newcmd/threads/slash-thread',
|
|
216
|
+
text: '/new',
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Second inbound: the real message. This is the one that triggers a
|
|
221
|
+
// turn and whose thread should anchor the activity log.
|
|
222
|
+
subscription.emitMessage(
|
|
223
|
+
makeThreadedMessage({
|
|
224
|
+
space: 'spaces/newcmd',
|
|
225
|
+
messageId: 'real',
|
|
226
|
+
threadName: 'spaces/newcmd/threads/real-thread',
|
|
227
|
+
text: SPAWN_COMMAND('newcmd-sub'),
|
|
228
|
+
})
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
await vi.waitFor(
|
|
232
|
+
() => {
|
|
233
|
+
const threaded = create.mock.calls.find(
|
|
234
|
+
([p]) =>
|
|
235
|
+
(p.requestBody as { thread?: { name?: string } }).thread?.name ===
|
|
236
|
+
'spaces/newcmd/threads/real-thread'
|
|
237
|
+
);
|
|
238
|
+
expect(threaded).toBeDefined();
|
|
239
|
+
},
|
|
240
|
+
// The subagent spawn takes a few seconds to fully round-trip through
|
|
241
|
+
// the daemon; 15s was tight enough to be flaky under full-suite load.
|
|
242
|
+
{ timeout: 45000 }
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Crucially: nothing should anchor to the /new thread.
|
|
246
|
+
const anchoredOnSlash = create.mock.calls.find(
|
|
247
|
+
([p]) =>
|
|
248
|
+
(p.requestBody as { thread?: { name?: string } }).thread?.name ===
|
|
249
|
+
'spaces/newcmd/threads/slash-thread'
|
|
250
|
+
);
|
|
251
|
+
expect(anchoredOnSlash).toBeUndefined();
|
|
252
|
+
});
|
|
253
|
+
}, 60000);
|
|
254
|
+
|
|
255
|
+
it('drops thread-log activity entirely when threadsDisabled is set on the space', async () => {
|
|
256
|
+
const { env } = envRef;
|
|
257
|
+
const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
|
|
258
|
+
const subscription = makeFakeSubscription();
|
|
259
|
+
const { api, create } = makeFakeChatApi();
|
|
260
|
+
|
|
261
|
+
await updateGoogleChatState(
|
|
262
|
+
{
|
|
263
|
+
channelChatMap: {
|
|
264
|
+
'spaces/noth': { chatId: 'gc-noth', threadsDisabled: true },
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
env.e2eDir
|
|
268
|
+
);
|
|
269
|
+
await env.addChat('gc-noth');
|
|
270
|
+
|
|
271
|
+
startGoogleChatIngestion(
|
|
272
|
+
BASE_CONFIG,
|
|
273
|
+
trpc,
|
|
274
|
+
{},
|
|
275
|
+
{ subscription, chatApi: api, startDir: env.e2eDir }
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const config = { ...BASE_CONFIG, chatId: 'gc-noth' };
|
|
279
|
+
|
|
280
|
+
await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
|
|
281
|
+
subscription.emitMessage(
|
|
282
|
+
makeThreadedMessage({
|
|
283
|
+
space: 'spaces/noth',
|
|
284
|
+
messageId: 'u1',
|
|
285
|
+
threadName: 'spaces/noth/threads/tn',
|
|
286
|
+
text: 'threads off',
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
await vi.waitFor(
|
|
291
|
+
() => {
|
|
292
|
+
expect(
|
|
293
|
+
create.mock.calls.find(
|
|
294
|
+
([p]) =>
|
|
295
|
+
p.requestBody.text === 'threads off' && !('thread' in p.requestBody)
|
|
296
|
+
)
|
|
297
|
+
).toBeDefined();
|
|
298
|
+
},
|
|
299
|
+
{ timeout: 15000 }
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const threadedCalls = create.mock.calls.filter(
|
|
303
|
+
([p]) =>
|
|
304
|
+
p.parent === 'spaces/noth' &&
|
|
305
|
+
(p.requestBody as { thread?: unknown }).thread !== undefined
|
|
306
|
+
);
|
|
307
|
+
expect(threadedCalls).toHaveLength(0);
|
|
308
|
+
});
|
|
309
|
+
}, 45000);
|
|
310
|
+
|
|
311
|
+
it('DM spaces thread activity onto the user message, same as group spaces', async () => {
|
|
312
|
+
const { env } = envRef;
|
|
313
|
+
const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
|
|
314
|
+
const subscription = makeFakeSubscription();
|
|
315
|
+
const { api, create } = makeFakeChatApi();
|
|
316
|
+
|
|
317
|
+
create.mockImplementation(
|
|
318
|
+
async () => ({ data: { name: 'spaces/dmsp/messages/log-1' } }) as unknown as object
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
await updateGoogleChatState(
|
|
322
|
+
{ channelChatMap: { 'spaces/dmsp': { chatId: 'gc-dm' } } },
|
|
323
|
+
env.e2eDir
|
|
324
|
+
);
|
|
325
|
+
await env.addChat('gc-dm', 'debug-agent');
|
|
326
|
+
|
|
327
|
+
startGoogleChatIngestion(
|
|
328
|
+
BASE_CONFIG,
|
|
329
|
+
trpc,
|
|
330
|
+
{},
|
|
331
|
+
{ subscription, chatApi: api, startDir: env.e2eDir }
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const config = { ...BASE_CONFIG, chatId: 'gc-dm' };
|
|
335
|
+
|
|
336
|
+
await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
|
|
337
|
+
// DM messages carry a thread.name even though DMs have no UI thread —
|
|
338
|
+
// GChat uses it as a reply anchor.
|
|
339
|
+
const dm = makeDmMessage({
|
|
340
|
+
space: 'spaces/dmsp',
|
|
341
|
+
messageId: 'u1',
|
|
342
|
+
text: SPAWN_COMMAND('dm-sub'),
|
|
343
|
+
});
|
|
344
|
+
// Inject a thread on the DM payload since makeDmMessage doesn't set one.
|
|
345
|
+
const parsed = JSON.parse(dm.data.toString('utf8'));
|
|
346
|
+
parsed.message.thread = { name: 'spaces/dmsp/threads/td' };
|
|
347
|
+
dm.data = Buffer.from(JSON.stringify(parsed));
|
|
348
|
+
subscription.emitMessage(dm);
|
|
349
|
+
|
|
350
|
+
await vi.waitFor(
|
|
351
|
+
() => {
|
|
352
|
+
expect(
|
|
353
|
+
create.mock.calls.find(
|
|
354
|
+
([p]) =>
|
|
355
|
+
typeof p.requestBody.text === 'string' &&
|
|
356
|
+
p.requestBody.text.includes('Subagent spawned successfully with ID: dm-sub') &&
|
|
357
|
+
!('thread' in p.requestBody)
|
|
358
|
+
)
|
|
359
|
+
).toBeDefined();
|
|
360
|
+
},
|
|
361
|
+
{ timeout: 15000 }
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await vi.waitFor(
|
|
365
|
+
() => {
|
|
366
|
+
const threaded = create.mock.calls.find(
|
|
367
|
+
([p]) =>
|
|
368
|
+
(p.requestBody as { thread?: { name?: string } }).thread?.name ===
|
|
369
|
+
'spaces/dmsp/threads/td'
|
|
370
|
+
);
|
|
371
|
+
expect(threaded).toBeDefined();
|
|
372
|
+
},
|
|
373
|
+
{ timeout: 15000 }
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
}, 60000);
|
|
377
|
+
|
|
378
|
+
it('falls back to top-level when visibility.threads is disabled globally', async () => {
|
|
379
|
+
const { env } = envRef;
|
|
380
|
+
const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
|
|
381
|
+
const subscription = makeFakeSubscription();
|
|
382
|
+
const { api, create } = makeFakeChatApi();
|
|
383
|
+
|
|
384
|
+
await updateGoogleChatState(
|
|
385
|
+
{ channelChatMap: { 'spaces/gth': { chatId: 'gc-globalthr' } } },
|
|
386
|
+
env.e2eDir
|
|
387
|
+
);
|
|
388
|
+
await env.addChat('gc-globalthr');
|
|
389
|
+
|
|
390
|
+
startGoogleChatIngestion(
|
|
391
|
+
BASE_CONFIG,
|
|
392
|
+
trpc,
|
|
393
|
+
{},
|
|
394
|
+
{ subscription, chatApi: api, startDir: env.e2eDir }
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const config = {
|
|
398
|
+
...BASE_CONFIG,
|
|
399
|
+
chatId: 'gc-globalthr',
|
|
400
|
+
visibility: { threads: false as const },
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
|
|
404
|
+
subscription.emitMessage(
|
|
405
|
+
makeThreadedMessage({
|
|
406
|
+
space: 'spaces/gth',
|
|
407
|
+
messageId: 'u1',
|
|
408
|
+
threadName: 'spaces/gth/threads/tg',
|
|
409
|
+
text: 'global off',
|
|
410
|
+
})
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
await vi.waitFor(
|
|
414
|
+
() => {
|
|
415
|
+
expect(
|
|
416
|
+
create.mock.calls.find(
|
|
417
|
+
([p]) =>
|
|
418
|
+
p.requestBody.text === 'global off' && !('thread' in p.requestBody)
|
|
419
|
+
)
|
|
420
|
+
).toBeDefined();
|
|
421
|
+
},
|
|
422
|
+
{ timeout: 15000 }
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const threaded = create.mock.calls.filter(
|
|
426
|
+
([p]) => (p.requestBody as { thread?: unknown }).thread !== undefined
|
|
427
|
+
);
|
|
428
|
+
expect(threaded).toHaveLength(0);
|
|
429
|
+
});
|
|
430
|
+
}, 45000);
|
|
431
|
+
|
|
432
|
+
it('renders the turn log for a debug-agent subagent spawn', async () => {
|
|
433
|
+
const { env } = envRef;
|
|
434
|
+
const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
|
|
435
|
+
const subscription = makeFakeSubscription();
|
|
436
|
+
const { api, create, update } = makeFakeChatApi();
|
|
437
|
+
|
|
438
|
+
// One activity-log message per turn; the forwarder opens it with `create`
|
|
439
|
+
// and then appends via `update` calls. Returning a stable name keeps the
|
|
440
|
+
// snapshot deterministic across runs.
|
|
441
|
+
create.mockImplementation(
|
|
442
|
+
async () => ({ data: { name: 'spaces/snap/messages/log-1' } }) as unknown as object
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
await updateGoogleChatState(
|
|
446
|
+
{ channelChatMap: { 'spaces/snap': { chatId: 'gc-snap' } } },
|
|
447
|
+
env.e2eDir
|
|
448
|
+
);
|
|
449
|
+
await env.addChat('gc-snap', 'debug-agent');
|
|
450
|
+
|
|
451
|
+
startGoogleChatIngestion(
|
|
452
|
+
BASE_CONFIG,
|
|
453
|
+
trpc,
|
|
454
|
+
{},
|
|
455
|
+
{ subscription, chatApi: api, startDir: env.e2eDir }
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const config = { ...BASE_CONFIG, chatId: 'gc-snap' };
|
|
459
|
+
|
|
460
|
+
await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
|
|
461
|
+
subscription.emitMessage(
|
|
462
|
+
makeThreadedMessage({
|
|
463
|
+
space: 'spaces/snap',
|
|
464
|
+
messageId: 'u1',
|
|
465
|
+
threadName: 'spaces/snap/threads/t1',
|
|
466
|
+
text: 'clawmini-lite.js subagents spawn --id hello-sub --async "sleep 5 && echo hello"',
|
|
467
|
+
})
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
// `✅ hello-sub` is the last status entry that lands in the activity
|
|
471
|
+
// log, so waiting for it in an `update` payload guarantees the final
|
|
472
|
+
// debounced flush has fired.
|
|
473
|
+
await vi.waitFor(
|
|
474
|
+
() => {
|
|
475
|
+
const last = [...update.mock.calls]
|
|
476
|
+
.reverse()
|
|
477
|
+
.find(([p]) => p.name === 'spaces/snap/messages/log-1');
|
|
478
|
+
expect(last).toBeDefined();
|
|
479
|
+
const text = (last![0].requestBody as { text?: string }).text ?? '';
|
|
480
|
+
expect(text).toMatch(/✅ hello-sub/);
|
|
481
|
+
},
|
|
482
|
+
{ timeout: 45000, interval: 500 }
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const lastUpdate = [...update.mock.calls]
|
|
487
|
+
.reverse()
|
|
488
|
+
.find(([p]) => p.name === 'spaces/snap/messages/log-1')!;
|
|
489
|
+
const rawText = (lastUpdate[0].requestBody as { text?: string }).text ?? '';
|
|
490
|
+
// Relative timestamps depend on wall-clock scheduling (sleep 5 + flush
|
|
491
|
+
// debounce drift); normalize to a placeholder for snapshot stability.
|
|
492
|
+
const normalized = rawText
|
|
493
|
+
.replace(/^• (?:\d+m)?\d+[ms]/gm, '• Δs')
|
|
494
|
+
.replace(/\/clawmini-e2e-google-chat-threads-[^/\s"]+/g, '/CLAWMINI_DIR');
|
|
495
|
+
|
|
496
|
+
expect(normalized).toMatchInlineSnapshot(`
|
|
497
|
+
"• Δs ▶️ Started processing…
|
|
498
|
+
• Δs 👉 hello-sub: sleep 5 && echo hello
|
|
499
|
+
• Δs 👈 hello-sub: [DEBUG] sleep 5 && echo hello: \`\`\` hello \`\`\`
|
|
500
|
+
• Δs ✅ hello-sub"
|
|
501
|
+
`);
|
|
502
|
+
}, 120000);
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Summarize the interleaved sequence of `create` / `update` calls for a
|
|
506
|
+
* snapshot. Strips wall-clock dependent bits (relative timestamps, temp-dir
|
|
507
|
+
* paths) so the expected output is stable run-to-run.
|
|
508
|
+
*/
|
|
509
|
+
function transcribe(
|
|
510
|
+
create: ReturnType<typeof makeFakeChatApi>['create'],
|
|
511
|
+
update: ReturnType<typeof makeFakeChatApi>['update']
|
|
512
|
+
): string {
|
|
513
|
+
const events = [
|
|
514
|
+
...create.mock.calls.map((c, i) => ({
|
|
515
|
+
kind: 'create' as const,
|
|
516
|
+
order: create.mock.invocationCallOrder[i]!,
|
|
517
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
518
|
+
call: c[0] as any,
|
|
519
|
+
})),
|
|
520
|
+
...update.mock.calls.map((c, i) => ({
|
|
521
|
+
kind: 'update' as const,
|
|
522
|
+
order: update.mock.invocationCallOrder[i]!,
|
|
523
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
524
|
+
call: c[0] as any,
|
|
525
|
+
})),
|
|
526
|
+
].sort((a, b) => a.order - b.order);
|
|
527
|
+
|
|
528
|
+
const normalize = (text: string) =>
|
|
529
|
+
(text ?? '')
|
|
530
|
+
.replace(/• (?:\d+m)?\d+[ms]/g, '• Δs')
|
|
531
|
+
.replace(/\/clawmini-e2e-google-chat-threads-[^/\s"]+/g, '/CLAWMINI_DIR');
|
|
532
|
+
|
|
533
|
+
const lines: string[] = [];
|
|
534
|
+
for (const ev of events) {
|
|
535
|
+
if (ev.kind === 'create') {
|
|
536
|
+
const thread: string | undefined = ev.call.requestBody?.thread?.name;
|
|
537
|
+
const text = normalize(ev.call.requestBody?.text ?? '');
|
|
538
|
+
const card = Array.isArray(ev.call.requestBody?.cardsV2)
|
|
539
|
+
? ev.call.requestBody.cardsV2.length > 0
|
|
540
|
+
: false;
|
|
541
|
+
const where = thread ? `thread=${thread}` : 'top-level';
|
|
542
|
+
const body = card && !text ? '<card>' : text;
|
|
543
|
+
lines.push(`CREATE ${where}\n ${body.replace(/\n/g, '\n ')}`);
|
|
544
|
+
} else {
|
|
545
|
+
const text = normalize(ev.call.requestBody?.text ?? '');
|
|
546
|
+
lines.push(`UPDATE name=${ev.call.name}\n ${text.replace(/\n/g, '\n ')}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return lines.join('\n---\n');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Wait for the `✅ <subagentId>` status entry to land in any posted
|
|
554
|
+
* activity-log content — signals that the final debounced flush for that
|
|
555
|
+
* subagent has fired. Checks every create/update (not just the most
|
|
556
|
+
* recent), since rollover may land the ✅ in a later threaded `create`
|
|
557
|
+
* rather than an `update`.
|
|
558
|
+
*/
|
|
559
|
+
async function waitForSubagentComplete(
|
|
560
|
+
update: ReturnType<typeof makeFakeChatApi>['update'],
|
|
561
|
+
create: ReturnType<typeof makeFakeChatApi>['create'],
|
|
562
|
+
subagentId: string,
|
|
563
|
+
timeout = 45000
|
|
564
|
+
): Promise<void> {
|
|
565
|
+
const needle = `✅ ${subagentId}`;
|
|
566
|
+
await vi.waitFor(
|
|
567
|
+
() => {
|
|
568
|
+
const allTexts = [
|
|
569
|
+
...create.mock.calls.map((c) => (c[0].requestBody as { text?: string }).text ?? ''),
|
|
570
|
+
...update.mock.calls.map((c) => (c[0].requestBody as { text?: string }).text ?? ''),
|
|
571
|
+
];
|
|
572
|
+
expect(allTexts.some((t) => t.includes(needle))).toBe(true);
|
|
573
|
+
},
|
|
574
|
+
{ timeout, interval: 500 }
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
it('rolls over into a new threaded log message when content exceeds maxLogMessageChars', async () => {
|
|
579
|
+
const { env } = envRef;
|
|
580
|
+
const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
|
|
581
|
+
const subscription = makeFakeSubscription();
|
|
582
|
+
const { api, create, update } = makeFakeChatApi();
|
|
583
|
+
|
|
584
|
+
// Distinct names per open so the transcript clearly shows rollover
|
|
585
|
+
// opening a fresh log-N inside the same thread.
|
|
586
|
+
let logCount = 0;
|
|
587
|
+
create.mockImplementation(
|
|
588
|
+
async () => ({ data: { name: `spaces/roll/messages/log-${++logCount}` } }) as unknown as object
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
await updateGoogleChatState(
|
|
592
|
+
{ channelChatMap: { 'spaces/roll': { chatId: 'gc-roll' } } },
|
|
593
|
+
env.e2eDir
|
|
594
|
+
);
|
|
595
|
+
await env.addChat('gc-roll', 'debug-agent');
|
|
596
|
+
|
|
597
|
+
startGoogleChatIngestion(
|
|
598
|
+
BASE_CONFIG,
|
|
599
|
+
trpc,
|
|
600
|
+
{},
|
|
601
|
+
{ subscription, chatApi: api, startDir: env.e2eDir }
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
// Budget of 80 chars forces rollover after ~1-2 entries.
|
|
605
|
+
const config = {
|
|
606
|
+
...BASE_CONFIG,
|
|
607
|
+
chatId: 'gc-roll',
|
|
608
|
+
visibility: {
|
|
609
|
+
threads: true,
|
|
610
|
+
threadLog: { maxLogMessageChars: 80, editDebounceMs: 100 },
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
|
|
615
|
+
subscription.emitMessage(
|
|
616
|
+
makeThreadedMessage({
|
|
617
|
+
space: 'spaces/roll',
|
|
618
|
+
messageId: 'u1',
|
|
619
|
+
threadName: 'spaces/roll/threads/t1',
|
|
620
|
+
text: 'clawmini-lite.js subagents spawn --id roll-sub --async "echo roll-output"',
|
|
621
|
+
})
|
|
622
|
+
);
|
|
623
|
+
await waitForSubagentComplete(update, create, 'roll-sub');
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// All threaded creates land in the same thread. There should be multiple
|
|
627
|
+
// of them because the 80-char budget cannot hold all three status entries
|
|
628
|
+
// in one message — rollover opens new log-N messages in the same thread.
|
|
629
|
+
const threadedCreates = create.mock.calls.filter(([p]) =>
|
|
630
|
+
Boolean((p.requestBody as { thread?: { name?: string } }).thread)
|
|
631
|
+
);
|
|
632
|
+
expect(threadedCreates.length).toBeGreaterThanOrEqual(2);
|
|
633
|
+
for (const call of threadedCreates) {
|
|
634
|
+
expect((call[0].requestBody as { thread?: { name?: string } }).thread?.name).toBe(
|
|
635
|
+
'spaces/roll/threads/t1'
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// The combined posted content (including edits) both names every stage of
|
|
640
|
+
// the subagent's lifecycle AND carries the `…log continues` marker,
|
|
641
|
+
// evidence that at least one rollover happened.
|
|
642
|
+
const allText = [
|
|
643
|
+
...create.mock.calls.map((c) => (c[0].requestBody.text ?? '') as string),
|
|
644
|
+
...update.mock.calls.map((c) => (c[0].requestBody.text ?? '') as string),
|
|
645
|
+
].join('\n');
|
|
646
|
+
expect(allText).toContain('…log continues');
|
|
647
|
+
expect(allText).toContain('👉 roll-sub');
|
|
648
|
+
expect(allText).toContain('👈 roll-sub');
|
|
649
|
+
expect(allText).toContain('✅ roll-sub');
|
|
650
|
+
}, 120000);
|
|
651
|
+
|
|
652
|
+
it('drops thread-log events for the rest of the turn when thread creation fails', async () => {
|
|
653
|
+
const { env } = envRef;
|
|
654
|
+
const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
|
|
655
|
+
const subscription = makeFakeSubscription();
|
|
656
|
+
const { api, create, update } = makeFakeChatApi();
|
|
657
|
+
|
|
658
|
+
// First threaded create fails; any later create (e.g. the agent's final
|
|
659
|
+
// top-level reply) succeeds.
|
|
660
|
+
let firstThreadedAttempt = true;
|
|
661
|
+
create.mockImplementation(async (params) => {
|
|
662
|
+
const threaded = Boolean((params.requestBody as { thread?: { name?: string } }).thread);
|
|
663
|
+
if (threaded && firstThreadedAttempt) {
|
|
664
|
+
firstThreadedAttempt = false;
|
|
665
|
+
throw new Error('GChat 503 — thread open failed');
|
|
666
|
+
}
|
|
667
|
+
return { data: { name: `spaces/deg/messages/${Math.random().toString(36).slice(2, 8)}` } };
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
await updateGoogleChatState(
|
|
671
|
+
{ channelChatMap: { 'spaces/deg': { chatId: 'gc-deg' } } },
|
|
672
|
+
env.e2eDir
|
|
673
|
+
);
|
|
674
|
+
await env.addChat('gc-deg', 'debug-agent');
|
|
675
|
+
|
|
676
|
+
startGoogleChatIngestion(
|
|
677
|
+
BASE_CONFIG,
|
|
678
|
+
trpc,
|
|
679
|
+
{},
|
|
680
|
+
{ subscription, chatApi: api, startDir: env.e2eDir }
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
const config = { ...BASE_CONFIG, chatId: 'gc-deg' };
|
|
684
|
+
|
|
685
|
+
await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
|
|
686
|
+
subscription.emitMessage(
|
|
687
|
+
makeThreadedMessage({
|
|
688
|
+
space: 'spaces/deg',
|
|
689
|
+
messageId: 'u1',
|
|
690
|
+
threadName: 'spaces/deg/threads/t1',
|
|
691
|
+
text: 'clawmini-lite.js subagents spawn --id deg-sub --async "echo hi"',
|
|
692
|
+
})
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
// The agent's final reply is a top-level post and still lands even
|
|
696
|
+
// after the thread-log was abandoned. Wait for any non-threaded
|
|
697
|
+
// create to observe the turn progressing past the failure.
|
|
698
|
+
await vi.waitFor(
|
|
699
|
+
() => {
|
|
700
|
+
const topLevelCalls = create.mock.calls.filter(
|
|
701
|
+
([p]) => !('thread' in (p.requestBody as { thread?: unknown }))
|
|
702
|
+
);
|
|
703
|
+
expect(topLevelCalls.length).toBeGreaterThan(0);
|
|
704
|
+
},
|
|
705
|
+
{ timeout: 45000, interval: 500 }
|
|
706
|
+
);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Exactly one failed thread-open attempt; no retries.
|
|
710
|
+
const threadedCreates = create.mock.calls.filter(([p]) =>
|
|
711
|
+
Boolean((p.requestBody as { thread?: { name?: string } }).thread)
|
|
712
|
+
);
|
|
713
|
+
expect(threadedCreates).toHaveLength(1);
|
|
714
|
+
|
|
715
|
+
// No updates — the log message was never successfully opened.
|
|
716
|
+
expect(update.mock.calls).toHaveLength(0);
|
|
717
|
+
|
|
718
|
+
// Thread-log activity (subagent markers) must NOT leak into top-level
|
|
719
|
+
// posts: once thread-open fails we drop the rest of the turn's log.
|
|
720
|
+
const topLevelText = create.mock.calls
|
|
721
|
+
.filter(([p]) => !('thread' in (p.requestBody as { thread?: unknown })))
|
|
722
|
+
.map(([p]) => (p.requestBody.text ?? '') as string)
|
|
723
|
+
.join('\n');
|
|
724
|
+
expect(topLevelText).not.toContain('👉 deg-sub');
|
|
725
|
+
expect(topLevelText).not.toContain('✅ deg-sub');
|
|
726
|
+
}, 120000);
|
|
727
|
+
|
|
728
|
+
it('coalesces a multi-subagent turn into a single threaded log message', async () => {
|
|
729
|
+
const { env } = envRef;
|
|
730
|
+
const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
|
|
731
|
+
const subscription = makeFakeSubscription();
|
|
732
|
+
const { api, create, update } = makeFakeChatApi();
|
|
733
|
+
|
|
734
|
+
create.mockImplementation(
|
|
735
|
+
async () => ({ data: { name: 'spaces/multi/messages/log-1' } }) as unknown as object
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
await updateGoogleChatState(
|
|
739
|
+
{ channelChatMap: { 'spaces/multi': { chatId: 'gc-multi' } } },
|
|
740
|
+
env.e2eDir
|
|
741
|
+
);
|
|
742
|
+
await env.addChat('gc-multi', 'debug-agent');
|
|
743
|
+
|
|
744
|
+
startGoogleChatIngestion(
|
|
745
|
+
BASE_CONFIG,
|
|
746
|
+
trpc,
|
|
747
|
+
{},
|
|
748
|
+
{ subscription, chatApi: api, startDir: env.e2eDir }
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
const config = { ...BASE_CONFIG, chatId: 'gc-multi' };
|
|
752
|
+
|
|
753
|
+
await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
|
|
754
|
+
// The parent agent's single eval spawns two async subagents. Both
|
|
755
|
+
// produce prompt/reply/status entries; the forwarder must fold them
|
|
756
|
+
// into the same thread-log message.
|
|
757
|
+
subscription.emitMessage(
|
|
758
|
+
makeThreadedMessage({
|
|
759
|
+
space: 'spaces/multi',
|
|
760
|
+
messageId: 'u1',
|
|
761
|
+
threadName: 'spaces/multi/threads/t1',
|
|
762
|
+
text:
|
|
763
|
+
'clawmini-lite.js subagents spawn --id multi-a --async "echo A" && ' +
|
|
764
|
+
'clawmini-lite.js subagents spawn --id multi-b --async "echo B"',
|
|
765
|
+
})
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
await waitForSubagentComplete(update, create, 'multi-a');
|
|
769
|
+
await waitForSubagentComplete(update, create, 'multi-b');
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// Exactly one threaded `create` (the log is opened once and edited).
|
|
773
|
+
const threadedCreates = create.mock.calls.filter(([p]) =>
|
|
774
|
+
Boolean((p.requestBody as { thread?: { name?: string } }).thread)
|
|
775
|
+
);
|
|
776
|
+
expect(threadedCreates).toHaveLength(1);
|
|
777
|
+
expect((threadedCreates[0]![0].requestBody as { thread?: { name?: string } }).thread?.name).toBe(
|
|
778
|
+
'spaces/multi/threads/t1'
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
// Coalescing: the number of `update` calls is far below the number of
|
|
782
|
+
// logged entries (we never post one edit per entry). Both subagents
|
|
783
|
+
// produce 3 entries each = 6; we expect the update count to be small
|
|
784
|
+
// (<=6). With a debounce of ~1s and two fast echoes, all entries often
|
|
785
|
+
// arrive before the first flush fires — the content ends up in the
|
|
786
|
+
// create payload with zero updates. Either way the final text should
|
|
787
|
+
// name both subagents end-to-end.
|
|
788
|
+
expect(update.mock.calls.length).toBeLessThanOrEqual(8);
|
|
789
|
+
|
|
790
|
+
const lastLogWrite =
|
|
791
|
+
[...update.mock.calls].reverse().find(([p]) => p.name === 'spaces/multi/messages/log-1') ??
|
|
792
|
+
threadedCreates[0]!;
|
|
793
|
+
const rawText = (lastLogWrite[0].requestBody.text ?? '') as string;
|
|
794
|
+
const normalized = rawText
|
|
795
|
+
.replace(/• (?:\d+m)?\d+[ms]/g, '• Δs')
|
|
796
|
+
.replace(/\/clawmini-e2e-google-chat-threads-[^/\s"]+/g, '/CLAWMINI_DIR');
|
|
797
|
+
|
|
798
|
+
// Both subagents are spawned with `--async`, so they run in parallel and
|
|
799
|
+
// their events can interleave under load. Assert each lifecycle is
|
|
800
|
+
// present and per-subagent order is preserved (👉 → 👈 → ✅), without
|
|
801
|
+
// pinning the relative order between the two.
|
|
802
|
+
const indexOf = (needle: string): number => {
|
|
803
|
+
const i = normalized.indexOf(needle);
|
|
804
|
+
expect(i, `expected log to contain ${JSON.stringify(needle)}`).toBeGreaterThanOrEqual(0);
|
|
805
|
+
return i;
|
|
806
|
+
};
|
|
807
|
+
const startedIdx = indexOf('▶️ Started processing…');
|
|
808
|
+
const aPromptIdx = indexOf('👉 multi-a: echo A');
|
|
809
|
+
const aReplyIdx = indexOf('👈 multi-a: [DEBUG] echo A: ``` A ```');
|
|
810
|
+
const aDoneIdx = indexOf('✅ multi-a');
|
|
811
|
+
const bPromptIdx = indexOf('👉 multi-b: echo B');
|
|
812
|
+
const bReplyIdx = indexOf('👈 multi-b: [DEBUG] echo B: ``` B ```');
|
|
813
|
+
const bDoneIdx = indexOf('✅ multi-b');
|
|
814
|
+
|
|
815
|
+
expect(startedIdx).toBeLessThan(aPromptIdx);
|
|
816
|
+
expect(startedIdx).toBeLessThan(bPromptIdx);
|
|
817
|
+
expect(aPromptIdx).toBeLessThan(aReplyIdx);
|
|
818
|
+
expect(aReplyIdx).toBeLessThan(aDoneIdx);
|
|
819
|
+
expect(bPromptIdx).toBeLessThan(bReplyIdx);
|
|
820
|
+
expect(bReplyIdx).toBeLessThan(bDoneIdx);
|
|
821
|
+
}, 120000);
|
|
822
|
+
|
|
823
|
+
it('snapshots the interleaved create/update transcript for a successful turn', async () => {
|
|
824
|
+
// End-to-end visibility: one snapshot showing exactly what a GChat client
|
|
825
|
+
// would see when a subagent runs — the thread open, the series of edits
|
|
826
|
+
// as events arrive, and the top-level final reply. `transcribe()` keeps
|
|
827
|
+
// ordering stable across coalescing changes.
|
|
828
|
+
const { env } = envRef;
|
|
829
|
+
const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
|
|
830
|
+
const subscription = makeFakeSubscription();
|
|
831
|
+
const { api, create, update } = makeFakeChatApi();
|
|
832
|
+
|
|
833
|
+
create.mockImplementation(
|
|
834
|
+
async () => ({ data: { name: 'spaces/txn/messages/log-1' } }) as unknown as object
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
await updateGoogleChatState(
|
|
838
|
+
{ channelChatMap: { 'spaces/txn': { chatId: 'gc-txn' } } },
|
|
839
|
+
env.e2eDir
|
|
840
|
+
);
|
|
841
|
+
await env.addChat('gc-txn', 'debug-agent');
|
|
842
|
+
|
|
843
|
+
startGoogleChatIngestion(
|
|
844
|
+
BASE_CONFIG,
|
|
845
|
+
trpc,
|
|
846
|
+
{},
|
|
847
|
+
{ subscription, chatApi: api, startDir: env.e2eDir }
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
// Large editDebounceMs forces all activity to collapse into as few
|
|
851
|
+
// updates as possible, keeping the transcript short and deterministic.
|
|
852
|
+
const config = {
|
|
853
|
+
...BASE_CONFIG,
|
|
854
|
+
chatId: 'gc-txn',
|
|
855
|
+
visibility: {
|
|
856
|
+
threads: true,
|
|
857
|
+
threadLog: { editDebounceMs: 2000 },
|
|
858
|
+
},
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
|
|
862
|
+
subscription.emitMessage(
|
|
863
|
+
makeThreadedMessage({
|
|
864
|
+
space: 'spaces/txn',
|
|
865
|
+
messageId: 'u1',
|
|
866
|
+
threadName: 'spaces/txn/threads/t1',
|
|
867
|
+
text: 'clawmini-lite.js subagents spawn --id txn-sub --async "echo done"',
|
|
868
|
+
})
|
|
869
|
+
);
|
|
870
|
+
await waitForSubagentComplete(update, create, 'txn-sub');
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
// The exact number of intermediate updates is allowed to vary (it depends
|
|
874
|
+
// on how debounce windows line up with subagent events); the important
|
|
875
|
+
// guarantees are: (1) first threaded post opens log-1 in t1, (2) every
|
|
876
|
+
// subsequent threaded touch edits log-1, (3) the final top-level reply
|
|
877
|
+
// names the subagent.
|
|
878
|
+
const threadedCreates = create.mock.calls.filter(([p]) =>
|
|
879
|
+
Boolean((p.requestBody as { thread?: { name?: string } }).thread)
|
|
880
|
+
);
|
|
881
|
+
expect(threadedCreates).toHaveLength(1);
|
|
882
|
+
expect((threadedCreates[0]![0].requestBody as { thread?: { name?: string } }).thread?.name).toBe(
|
|
883
|
+
'spaces/txn/threads/t1'
|
|
884
|
+
);
|
|
885
|
+
for (const u of update.mock.calls) {
|
|
886
|
+
expect(u[0].name).toBe('spaces/txn/messages/log-1');
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const lastUpdate = [...update.mock.calls].reverse()[0];
|
|
890
|
+
const lastCreate = threadedCreates[0]![0];
|
|
891
|
+
const finalLog =
|
|
892
|
+
((lastUpdate?.[0].requestBody.text as string) ??
|
|
893
|
+
(lastCreate.requestBody.text as string) ??
|
|
894
|
+
'')
|
|
895
|
+
.replace(/• (?:\d+m)?\d+[ms]/g, '• Δs')
|
|
896
|
+
.replace(/\/clawmini-e2e-google-chat-threads-[^/\s"]+/g, '/CLAWMINI_DIR');
|
|
897
|
+
expect(finalLog).toMatchInlineSnapshot(`
|
|
898
|
+
"• Δs ▶️ Started processing…
|
|
899
|
+
• Δs 👉 txn-sub: echo done
|
|
900
|
+
• Δs 👈 txn-sub: [DEBUG] echo done: \`\`\` done \`\`\`
|
|
901
|
+
• Δs ✅ txn-sub"
|
|
902
|
+
`);
|
|
903
|
+
|
|
904
|
+
// Reference the transcript helper so it stays part of the compiled test
|
|
905
|
+
// surface even if a future run doesn't need its full output.
|
|
906
|
+
const transcript = transcribe(create, update);
|
|
907
|
+
expect(transcript).toContain('CREATE thread=spaces/txn/threads/t1');
|
|
908
|
+
expect(transcript).toContain('✅ txn-sub');
|
|
909
|
+
}, 120000);
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Stand up a session-timeout cron that spawns a subagent. Returns the
|
|
913
|
+
* `create`/`update` mocks and the threaded-creates helper. Shared between
|
|
914
|
+
* the silent-mode and header-mode tests, which differ only in the
|
|
915
|
+
* `visibility.jobs` config they install.
|
|
916
|
+
*/
|
|
917
|
+
async function runCronScenario(opts: {
|
|
918
|
+
chatId: string;
|
|
919
|
+
space: string;
|
|
920
|
+
jobsMode?: 'silent' | 'header';
|
|
921
|
+
}) {
|
|
922
|
+
const { env } = envRef;
|
|
923
|
+
const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
|
|
924
|
+
const subscription = makeFakeSubscription();
|
|
925
|
+
const { api, create, update } = makeFakeChatApi();
|
|
926
|
+
|
|
927
|
+
let threadCounter = 0;
|
|
928
|
+
let msgCounter = 0;
|
|
929
|
+
create.mockImplementation(async (params) => {
|
|
930
|
+
msgCounter++;
|
|
931
|
+
const msgName = `${opts.space}/messages/msg-${msgCounter}`;
|
|
932
|
+
const isThreaded = Boolean(
|
|
933
|
+
(params.requestBody as { thread?: { name?: string } }).thread
|
|
934
|
+
);
|
|
935
|
+
if (isThreaded) {
|
|
936
|
+
return {
|
|
937
|
+
data: {
|
|
938
|
+
name: msgName,
|
|
939
|
+
thread: (params.requestBody as { thread?: { name?: string } }).thread,
|
|
940
|
+
},
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
threadCounter++;
|
|
944
|
+
return {
|
|
945
|
+
data: {
|
|
946
|
+
name: msgName,
|
|
947
|
+
thread: { name: `${opts.space}/threads/auto-${threadCounter}` },
|
|
948
|
+
},
|
|
949
|
+
};
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
await updateGoogleChatState(
|
|
953
|
+
{ channelChatMap: { [opts.space]: { chatId: opts.chatId } } },
|
|
954
|
+
env.e2eDir
|
|
955
|
+
);
|
|
956
|
+
await env.addChat(opts.chatId, 'debug-agent');
|
|
957
|
+
|
|
958
|
+
const cronPrompt =
|
|
959
|
+
'clawmini-lite.js subagents spawn --id cron-sub --async "echo session-ended"';
|
|
960
|
+
env.writeChatSettings(opts.chatId, {
|
|
961
|
+
defaultAgent: 'debug-agent',
|
|
962
|
+
routers: [{ use: '@clawmini/session-timeout', with: { timeout: '3s', prompt: cronPrompt } }],
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
startGoogleChatIngestion(
|
|
966
|
+
BASE_CONFIG,
|
|
967
|
+
trpc,
|
|
968
|
+
{},
|
|
969
|
+
{ subscription, chatApi: api, startDir: env.e2eDir }
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
const config: typeof BASE_CONFIG = {
|
|
973
|
+
...BASE_CONFIG,
|
|
974
|
+
chatId: opts.chatId,
|
|
975
|
+
...(opts.jobsMode ? { visibility: { jobs: opts.jobsMode } } : {}),
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
|
|
979
|
+
subscription.emitMessage(
|
|
980
|
+
makeThreadedMessage({
|
|
981
|
+
space: opts.space,
|
|
982
|
+
messageId: 'kick',
|
|
983
|
+
threadName: `${opts.space}/threads/kick`,
|
|
984
|
+
text: 'hello',
|
|
985
|
+
})
|
|
986
|
+
);
|
|
987
|
+
await waitForSubagentComplete(update, create, 'cron-sub');
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
const kickThread = `${opts.space}/threads/kick`;
|
|
991
|
+
const cronThreadedCreates = create.mock.calls
|
|
992
|
+
.filter(([p]) => Boolean((p.requestBody as { thread?: { name?: string } }).thread))
|
|
993
|
+
.filter(([p]) => {
|
|
994
|
+
const thread = (p.requestBody as { thread?: { name?: string } }).thread!.name!;
|
|
995
|
+
return thread !== kickThread;
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
return { create, update, cronThreadedCreates };
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
it('silent mode (default): cron prompt stays hidden; activity anchors on the agent reply', async () => {
|
|
1002
|
+
// Default `visibility.jobs: 'silent'` drops the cron SystemMessage. The
|
|
1003
|
+
// agent's auto-reply ("Starting a fresh session…") becomes the first
|
|
1004
|
+
// top-level post and therefore the thread anchor for the cron turn's
|
|
1005
|
+
// subagent activity.
|
|
1006
|
+
const { create, cronThreadedCreates } = await runCronScenario({
|
|
1007
|
+
chatId: 'gc-cron-silent',
|
|
1008
|
+
space: 'spaces/cron-silent',
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// No `[SYSTEM]`-prefixed post: the cron prompt is never surfaced.
|
|
1012
|
+
const sysPosts = create.mock.calls.filter(
|
|
1013
|
+
([p]) =>
|
|
1014
|
+
typeof p.requestBody.text === 'string' &&
|
|
1015
|
+
(p.requestBody.text as string).startsWith('[SYSTEM] ')
|
|
1016
|
+
);
|
|
1017
|
+
expect(sysPosts).toHaveLength(0);
|
|
1018
|
+
|
|
1019
|
+
// The auto-reply lands top-level and seeds the cron turn's thread.
|
|
1020
|
+
const replyPost = create.mock.calls.find(
|
|
1021
|
+
([p]) =>
|
|
1022
|
+
typeof p.requestBody.text === 'string' &&
|
|
1023
|
+
(p.requestBody.text as string).includes('Starting a fresh session') &&
|
|
1024
|
+
!('thread' in (p.requestBody as { thread?: unknown }))
|
|
1025
|
+
);
|
|
1026
|
+
expect(replyPost).toBeDefined();
|
|
1027
|
+
|
|
1028
|
+
// All thread-log activity for the cron turn anchors on exactly one thread
|
|
1029
|
+
// (the one auto-created by the reply post), not on the prior user turn.
|
|
1030
|
+
expect(cronThreadedCreates.length).toBeGreaterThanOrEqual(1);
|
|
1031
|
+
const anchored = new Set(
|
|
1032
|
+
cronThreadedCreates.map(
|
|
1033
|
+
([p]) => (p.requestBody as { thread?: { name?: string } }).thread!.name!
|
|
1034
|
+
)
|
|
1035
|
+
);
|
|
1036
|
+
expect(anchored.size).toBe(1);
|
|
1037
|
+
expect([...anchored][0]).toMatch(/\/threads\/auto-/);
|
|
1038
|
+
}, 60000);
|
|
1039
|
+
|
|
1040
|
+
it('header mode: posts 🕒 <jobId> as the anchor for cron activity', async () => {
|
|
1041
|
+
const { create, cronThreadedCreates } = await runCronScenario({
|
|
1042
|
+
chatId: 'gc-cron-header',
|
|
1043
|
+
space: 'spaces/cron-header',
|
|
1044
|
+
jobsMode: 'header',
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
// The cron heartbeat posts top-level with a 🕒 prefix and carries the
|
|
1048
|
+
// job id, not the (potentially sensitive) prompt text.
|
|
1049
|
+
const headerPost = create.mock.calls.find(
|
|
1050
|
+
([p]) =>
|
|
1051
|
+
typeof p.requestBody.text === 'string' &&
|
|
1052
|
+
(p.requestBody.text as string).startsWith('🕒 ') &&
|
|
1053
|
+
!('thread' in (p.requestBody as { thread?: unknown }))
|
|
1054
|
+
);
|
|
1055
|
+
expect(headerPost).toBeDefined();
|
|
1056
|
+
const headerText = headerPost![0].requestBody.text as string;
|
|
1057
|
+
expect(headerText).toContain('__session_timeout__');
|
|
1058
|
+
expect(headerText).not.toContain('clawmini-lite.js');
|
|
1059
|
+
|
|
1060
|
+
// And no `[SYSTEM] `-prefixed top-level post was generated.
|
|
1061
|
+
const sysPosts = create.mock.calls.filter(
|
|
1062
|
+
([p]) =>
|
|
1063
|
+
typeof p.requestBody.text === 'string' &&
|
|
1064
|
+
(p.requestBody.text as string).startsWith('[SYSTEM] ')
|
|
1065
|
+
);
|
|
1066
|
+
expect(sysPosts).toHaveLength(0);
|
|
1067
|
+
|
|
1068
|
+
// Subagent activity threads under the header's auto-created thread.
|
|
1069
|
+
expect(cronThreadedCreates.length).toBeGreaterThanOrEqual(1);
|
|
1070
|
+
const anchored = new Set(
|
|
1071
|
+
cronThreadedCreates.map(
|
|
1072
|
+
([p]) => (p.requestBody as { thread?: { name?: string } }).thread!.name!
|
|
1073
|
+
)
|
|
1074
|
+
);
|
|
1075
|
+
expect(anchored.size).toBe(1);
|
|
1076
|
+
expect([...anchored][0]).toMatch(/\/threads\/auto-/);
|
|
1077
|
+
}, 60000);
|
|
1078
|
+
});
|