clawmini 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/{G_zz-Gou.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.CYS8iApT.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.Dr0ot9sV.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.BBGQ_i84.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 +42 -13
- 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 -176
- package/web/.svelte-kit/generated/server/internal.js +1 -1
- package/web/.svelte-kit/output/client/.vite/manifest.json +127 -137
- 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/{G_zz-Gou.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.CYS8iApT.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.Dr0ot9sV.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.BBGQ_i84.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/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/chunks/bBmtyQMj.js +0 -1
- package/dist/web/_app/immutable/entry/app.CJmSwntr.js +0 -2
- package/dist/web/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
- package/dist/web/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
- package/dist/web/_app/immutable/nodes/4.oBhvQhcA.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/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/chunks/bBmtyQMj.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/app.CJmSwntr.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.oBhvQhcA.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,308 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
TestEnvironment,
|
|
6
|
+
type ChatSubscription,
|
|
7
|
+
type SystemMessage,
|
|
8
|
+
policyWith,
|
|
9
|
+
} from '../_helpers/test-environment.js';
|
|
10
|
+
|
|
11
|
+
describe('Policy Flows E2E', () => {
|
|
12
|
+
let env: TestEnvironment;
|
|
13
|
+
let chat: ChatSubscription | undefined;
|
|
14
|
+
let secondChat: ChatSubscription | undefined;
|
|
15
|
+
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
env = new TestEnvironment('e2e-policy-flows');
|
|
18
|
+
await env.setup();
|
|
19
|
+
await env.setupSubagentEnv({
|
|
20
|
+
policies: {
|
|
21
|
+
'test-cmd': {
|
|
22
|
+
description: 'A test policy',
|
|
23
|
+
command: 'echo',
|
|
24
|
+
args: ['policy executed'],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}, 30000);
|
|
29
|
+
|
|
30
|
+
afterAll(() => env.teardown(), 30000);
|
|
31
|
+
afterEach(() => env.disconnectAll());
|
|
32
|
+
|
|
33
|
+
const sanitize = (content: string, reqId: string) =>
|
|
34
|
+
content.replace(new RegExp(reqId, 'g'), '<REQ_ID>');
|
|
35
|
+
|
|
36
|
+
const approvedStdout =
|
|
37
|
+
'Request <REQ_ID> approved.\n\n<stdout>\npolicy executed\n</stdout>\n\n<stderr></stderr>\n\nExit Code: 0';
|
|
38
|
+
|
|
39
|
+
type RouteCase = {
|
|
40
|
+
label: string;
|
|
41
|
+
chat: string;
|
|
42
|
+
spawn: string;
|
|
43
|
+
action: 'approve' | 'reject';
|
|
44
|
+
event: 'policy_approved' | 'policy_rejected';
|
|
45
|
+
subagentId: string | undefined;
|
|
46
|
+
expectedUserContent: string;
|
|
47
|
+
expectedActorContent: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const routeCases: RouteCase[] = [
|
|
51
|
+
{
|
|
52
|
+
label: 'subagent /reject',
|
|
53
|
+
chat: 'chat-reject-sub',
|
|
54
|
+
spawn:
|
|
55
|
+
'clawmini-lite.js subagents spawn --id sub-reject --async "clawmini-lite.js request test-cmd --async"',
|
|
56
|
+
action: 'reject',
|
|
57
|
+
event: 'policy_rejected',
|
|
58
|
+
subagentId: 'sub-reject',
|
|
59
|
+
expectedUserContent: 'Request <REQ_ID> (`test-cmd`) rejected. Reason: No reason provided',
|
|
60
|
+
expectedActorContent: 'Request <REQ_ID> rejected. Reason: No reason provided',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: 'subagent /approve',
|
|
64
|
+
chat: 'chat-approve-sub',
|
|
65
|
+
spawn:
|
|
66
|
+
'clawmini-lite.js subagents spawn --id sub-approve --async "clawmini-lite.js request test-cmd"',
|
|
67
|
+
action: 'approve',
|
|
68
|
+
event: 'policy_approved',
|
|
69
|
+
subagentId: 'sub-approve',
|
|
70
|
+
expectedUserContent: 'Request <REQ_ID> (`test-cmd`) approved.',
|
|
71
|
+
expectedActorContent: approvedStdout,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
label: 'main agent /reject',
|
|
75
|
+
chat: 'chat-reject-main',
|
|
76
|
+
spawn: 'clawmini-lite.js request test-cmd --async',
|
|
77
|
+
action: 'reject',
|
|
78
|
+
event: 'policy_rejected',
|
|
79
|
+
subagentId: undefined,
|
|
80
|
+
expectedUserContent: 'Request <REQ_ID> (`test-cmd`) rejected. Reason: No reason provided',
|
|
81
|
+
expectedActorContent: 'Request <REQ_ID> rejected. Reason: No reason provided',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
label: 'main agent /approve',
|
|
85
|
+
chat: 'chat-approve-main',
|
|
86
|
+
spawn: 'clawmini-lite.js request test-cmd',
|
|
87
|
+
action: 'approve',
|
|
88
|
+
event: 'policy_approved',
|
|
89
|
+
subagentId: undefined,
|
|
90
|
+
expectedUserContent: 'Request <REQ_ID> (`test-cmd`) approved.',
|
|
91
|
+
expectedActorContent: approvedStdout,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
it.each(routeCases)(
|
|
96
|
+
'routes policy notifications ($label)',
|
|
97
|
+
async ({ chat: chatId, spawn, action, event, subagentId, expectedUserContent, expectedActorContent }) => {
|
|
98
|
+
await env.addChat(chatId);
|
|
99
|
+
chat = await env.connect(chatId);
|
|
100
|
+
|
|
101
|
+
await env.sendMessage(spawn, { chat: chatId, agent: 'debug-agent' });
|
|
102
|
+
|
|
103
|
+
const policy = await chat.waitForMessage(
|
|
104
|
+
policyWith()
|
|
105
|
+
);
|
|
106
|
+
const reqId = policy.requestId;
|
|
107
|
+
|
|
108
|
+
await env.sendMessage(`/${action} ${reqId}`, { chat: chatId });
|
|
109
|
+
|
|
110
|
+
const userNotif = await chat.waitForMessage(
|
|
111
|
+
(m): m is SystemMessage =>
|
|
112
|
+
m.role === 'system' && m.event === event && m.displayRole === 'agent'
|
|
113
|
+
);
|
|
114
|
+
expect(userNotif.subagentId).toBeUndefined();
|
|
115
|
+
expect(sanitize(userNotif.content, reqId)).toBe(expectedUserContent);
|
|
116
|
+
|
|
117
|
+
const actorNotif = await chat.waitForMessage(
|
|
118
|
+
(m): m is SystemMessage =>
|
|
119
|
+
m.role === 'system' &&
|
|
120
|
+
m.event === event &&
|
|
121
|
+
m.displayRole === 'user' &&
|
|
122
|
+
m.subagentId === subagentId
|
|
123
|
+
);
|
|
124
|
+
expect(sanitize(actorNotif.content, reqId)).toBe(expectedActorContent);
|
|
125
|
+
|
|
126
|
+
const reqPath = path.resolve(env.e2eDir, `.clawmini/tmp/requests/${reqId}.json`);
|
|
127
|
+
expect(fs.existsSync(reqPath)).toBe(false);
|
|
128
|
+
},
|
|
129
|
+
15000
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
it('should list pending requests in /pending output', async () => {
|
|
133
|
+
await env.addChat('chat-pending-list');
|
|
134
|
+
chat = await env.connect('chat-pending-list');
|
|
135
|
+
|
|
136
|
+
await env.sendMessage('clawmini-lite.js request test-cmd', {
|
|
137
|
+
chat: 'chat-pending-list',
|
|
138
|
+
agent: 'debug-agent',
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const reqId = (
|
|
142
|
+
await chat.waitForMessage(policyWith())
|
|
143
|
+
).requestId;
|
|
144
|
+
|
|
145
|
+
await env.sendMessage('/pending', { chat: 'chat-pending-list' });
|
|
146
|
+
|
|
147
|
+
const reply = await chat.waitForMessage(
|
|
148
|
+
(m): m is SystemMessage =>
|
|
149
|
+
m.role === 'system' &&
|
|
150
|
+
m.event === 'router' &&
|
|
151
|
+
typeof m.content === 'string' &&
|
|
152
|
+
m.content.includes('Pending Requests')
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(reply.content).toContain('Pending Requests (1):');
|
|
156
|
+
expect(reply.content).toContain(`- ID: ${reqId} | Command: test-cmd`);
|
|
157
|
+
}, 15000);
|
|
158
|
+
|
|
159
|
+
it('should include a custom reason in /reject notifications and delete the request file', async () => {
|
|
160
|
+
await env.addChat('chat-reject-reason');
|
|
161
|
+
chat = await env.connect('chat-reject-reason');
|
|
162
|
+
|
|
163
|
+
await env.sendMessage('clawmini-lite.js request test-cmd', {
|
|
164
|
+
chat: 'chat-reject-reason',
|
|
165
|
+
agent: 'debug-agent',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const reqId = (
|
|
169
|
+
await chat.waitForMessage(policyWith())
|
|
170
|
+
).requestId;
|
|
171
|
+
|
|
172
|
+
await env.sendMessage(`/reject ${reqId} command looked suspicious`, {
|
|
173
|
+
chat: 'chat-reject-reason',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const userMsg = await chat.waitForMessage(
|
|
177
|
+
(m): m is SystemMessage =>
|
|
178
|
+
m.role === 'system' && m.event === 'policy_rejected' && m.displayRole === 'agent'
|
|
179
|
+
);
|
|
180
|
+
expect(userMsg.content).toContain('command looked suspicious');
|
|
181
|
+
|
|
182
|
+
const agentMsg = await chat.waitForMessage(
|
|
183
|
+
(m): m is SystemMessage =>
|
|
184
|
+
m.role === 'system' && m.event === 'policy_rejected' && m.displayRole === 'user'
|
|
185
|
+
);
|
|
186
|
+
expect(agentMsg.content).toContain('command looked suspicious');
|
|
187
|
+
|
|
188
|
+
const reqPath = path.resolve(env.e2eDir, `.clawmini/tmp/requests/${reqId}.json`);
|
|
189
|
+
expect(fs.existsSync(reqPath)).toBe(false);
|
|
190
|
+
}, 15000);
|
|
191
|
+
|
|
192
|
+
describe('validation branches', () => {
|
|
193
|
+
it('should reply "Request not found" for /approve with an unknown id', async () => {
|
|
194
|
+
await env.addChat('chat-notfound');
|
|
195
|
+
chat = await env.connect('chat-notfound');
|
|
196
|
+
|
|
197
|
+
await env.sendMessage('/approve nonexistent-id', { chat: 'chat-notfound' });
|
|
198
|
+
|
|
199
|
+
await chat.waitForMessage(
|
|
200
|
+
(m): m is SystemMessage =>
|
|
201
|
+
m.role === 'system' &&
|
|
202
|
+
m.event === 'router' &&
|
|
203
|
+
typeof m.content === 'string' &&
|
|
204
|
+
m.content.includes('Request not found: nonexistent-id')
|
|
205
|
+
);
|
|
206
|
+
}, 15000);
|
|
207
|
+
|
|
208
|
+
it('should refuse cross-chat /approve', async () => {
|
|
209
|
+
await env.addChat('chat-owner');
|
|
210
|
+
await env.addChat('chat-intruder');
|
|
211
|
+
chat = await env.connect('chat-owner');
|
|
212
|
+
|
|
213
|
+
await env.sendMessage('clawmini-lite.js request test-cmd', {
|
|
214
|
+
chat: 'chat-owner',
|
|
215
|
+
agent: 'debug-agent',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const reqId = (
|
|
219
|
+
await chat.waitForMessage(policyWith())
|
|
220
|
+
).requestId;
|
|
221
|
+
|
|
222
|
+
secondChat = await env.connect('chat-intruder');
|
|
223
|
+
|
|
224
|
+
await env.sendMessage(`/approve ${reqId}`, { chat: 'chat-intruder' });
|
|
225
|
+
|
|
226
|
+
await secondChat.waitForMessage(
|
|
227
|
+
(m): m is SystemMessage =>
|
|
228
|
+
m.role === 'system' &&
|
|
229
|
+
m.event === 'router' &&
|
|
230
|
+
typeof m.content === 'string' &&
|
|
231
|
+
m.content.includes('Request belongs to a different chat')
|
|
232
|
+
);
|
|
233
|
+
}, 15000);
|
|
234
|
+
|
|
235
|
+
it('should refuse /approve on an already-approved request', async () => {
|
|
236
|
+
await env.addChat('chat-double-approve');
|
|
237
|
+
chat = await env.connect('chat-double-approve');
|
|
238
|
+
|
|
239
|
+
await env.sendMessage('clawmini-lite.js request test-cmd', {
|
|
240
|
+
chat: 'chat-double-approve',
|
|
241
|
+
agent: 'debug-agent',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const reqId = (
|
|
245
|
+
await chat.waitForMessage(policyWith())
|
|
246
|
+
).requestId;
|
|
247
|
+
|
|
248
|
+
await env.sendMessage(`/approve ${reqId}`, { chat: 'chat-double-approve' });
|
|
249
|
+
|
|
250
|
+
// Wait for the first approval to complete before sending a second one.
|
|
251
|
+
await chat.waitForMessage(
|
|
252
|
+
(m): m is SystemMessage => m.role === 'system' && m.event === 'policy_approved'
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
await env.sendMessage(`/approve ${reqId}`, { chat: 'chat-double-approve' });
|
|
256
|
+
|
|
257
|
+
// Approved requests are deleted, so the second /approve sees the request as gone.
|
|
258
|
+
await chat.waitForMessage(
|
|
259
|
+
(m): m is SystemMessage =>
|
|
260
|
+
m.role === 'system' &&
|
|
261
|
+
m.event === 'router' &&
|
|
262
|
+
typeof m.content === 'string' &&
|
|
263
|
+
m.content.includes(`Request not found: ${reqId}`)
|
|
264
|
+
);
|
|
265
|
+
}, 15000);
|
|
266
|
+
|
|
267
|
+
it('should reply "Policy not found" if the policy was removed after request creation', async () => {
|
|
268
|
+
const policiesPath = path.resolve(env.e2eDir, '.clawmini/policies.json');
|
|
269
|
+
const original = JSON.parse(fs.readFileSync(policiesPath, 'utf8'));
|
|
270
|
+
const mutated = JSON.parse(JSON.stringify(original));
|
|
271
|
+
mutated.policies['temp-cmd'] = {
|
|
272
|
+
description: 'Temporary',
|
|
273
|
+
command: 'echo',
|
|
274
|
+
args: ['temp'],
|
|
275
|
+
};
|
|
276
|
+
fs.writeFileSync(policiesPath, JSON.stringify(mutated));
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
await env.addChat('chat-policy-gone');
|
|
280
|
+
chat = await env.connect('chat-policy-gone');
|
|
281
|
+
|
|
282
|
+
await env.sendMessage('clawmini-lite.js request temp-cmd', {
|
|
283
|
+
chat: 'chat-policy-gone',
|
|
284
|
+
agent: 'debug-agent',
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const reqId = (
|
|
288
|
+
await chat.waitForMessage(policyWith())
|
|
289
|
+
).requestId;
|
|
290
|
+
|
|
291
|
+
// Remove the policy before /approve is processed
|
|
292
|
+
fs.writeFileSync(policiesPath, JSON.stringify(original));
|
|
293
|
+
|
|
294
|
+
await env.sendMessage(`/approve ${reqId}`, { chat: 'chat-policy-gone' });
|
|
295
|
+
|
|
296
|
+
await chat.waitForMessage(
|
|
297
|
+
(m): m is SystemMessage =>
|
|
298
|
+
m.role === 'system' &&
|
|
299
|
+
m.event === 'router' &&
|
|
300
|
+
typeof m.content === 'string' &&
|
|
301
|
+
m.content.includes('Policy not found: temp-cmd')
|
|
302
|
+
);
|
|
303
|
+
} finally {
|
|
304
|
+
fs.writeFileSync(policiesPath, JSON.stringify(original));
|
|
305
|
+
}
|
|
306
|
+
}, 15000);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { TestEnvironment } from '../_helpers/test-environment.js';
|
|
5
|
+
import type { PolicyRequest, RequestState } from '../../src/shared/policies.js';
|
|
6
|
+
|
|
7
|
+
describe('Policy Startup Cleanup E2E', () => {
|
|
8
|
+
let env: TestEnvironment;
|
|
9
|
+
let requestsDir: string;
|
|
10
|
+
|
|
11
|
+
const makeRequest = (id: string, state: RequestState): PolicyRequest => ({
|
|
12
|
+
id,
|
|
13
|
+
commandName: 'test-cmd',
|
|
14
|
+
args: [],
|
|
15
|
+
fileMappings: {},
|
|
16
|
+
state,
|
|
17
|
+
createdAt: Date.now(),
|
|
18
|
+
chatId: 'some-chat',
|
|
19
|
+
agentId: 'some-agent',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const writeRequest = (req: PolicyRequest) => {
|
|
23
|
+
fs.writeFileSync(path.join(requestsDir, `${req.id}.json`), JSON.stringify(req, null, 2));
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
env = new TestEnvironment('e2e-upgrade-cleanup');
|
|
28
|
+
await env.setup();
|
|
29
|
+
await env.init();
|
|
30
|
+
|
|
31
|
+
requestsDir = path.resolve(env.e2eDir, '.clawmini/tmp/requests');
|
|
32
|
+
fs.mkdirSync(requestsDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
writeRequest(makeRequest('PENDING1', 'Pending'));
|
|
35
|
+
writeRequest(makeRequest('APPROVED1', 'Approved'));
|
|
36
|
+
writeRequest(makeRequest('REJECTED1', 'Rejected'));
|
|
37
|
+
|
|
38
|
+
await env.up();
|
|
39
|
+
}, 30000);
|
|
40
|
+
|
|
41
|
+
afterAll(() => env.teardown(), 30000);
|
|
42
|
+
|
|
43
|
+
it('deletes completed request files on startup and keeps pending ones', () => {
|
|
44
|
+
expect(fs.existsSync(path.join(requestsDir, 'PENDING1.json'))).toBe(true);
|
|
45
|
+
expect(fs.existsSync(path.join(requestsDir, 'APPROVED1.json'))).toBe(false);
|
|
46
|
+
expect(fs.existsSync(path.join(requestsDir, 'REJECTED1.json'))).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
2
|
+
import { TestEnvironment, type ChatSubscription, commandMatching } from '../_helpers/test-environment.js';
|
|
3
|
+
|
|
4
|
+
describe('Session Timeout E2E', () => {
|
|
5
|
+
let env: TestEnvironment;
|
|
6
|
+
let chat: ChatSubscription | undefined;
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
env = new TestEnvironment('e2e-timeout');
|
|
10
|
+
await env.setup();
|
|
11
|
+
await env.runCli(['init', '--agent', 'test-agent', '--agent-template', 'debug']);
|
|
12
|
+
|
|
13
|
+
env.updateSettings({
|
|
14
|
+
routers: [{ use: '@clawmini/session-timeout', with: { timeout: '5s' } }],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
env.updateAgentSettings('test-agent', {
|
|
18
|
+
commands: {
|
|
19
|
+
new: 'echo "[DEBUG NEW $SESSION_ID] $CLAW_CLI_MESSAGE"',
|
|
20
|
+
append: 'echo "[DEBUG APPEND $SESSION_ID] $CLAW_CLI_MESSAGE"',
|
|
21
|
+
getSessionId: "node -e 'console.log(Math.random().toString(36).slice(2, 10))'",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await env.up();
|
|
26
|
+
}, 30000);
|
|
27
|
+
|
|
28
|
+
afterAll(() => env.teardown(), 30000);
|
|
29
|
+
afterEach(() => env.disconnectAll());
|
|
30
|
+
|
|
31
|
+
it('fires the timeout job and routes the next message to a new session', async () => {
|
|
32
|
+
// `init --agent test-agent` sets the default chat id to 'test-agent'.
|
|
33
|
+
chat = await env.connect('test-agent');
|
|
34
|
+
|
|
35
|
+
const { code } = await env.sendMessage('first message');
|
|
36
|
+
expect(code).toBe(0);
|
|
37
|
+
|
|
38
|
+
const { stdout: jobsList } = await env.runCli(['jobs', 'list']);
|
|
39
|
+
expect(jobsList).toContain('__session_timeout__');
|
|
40
|
+
|
|
41
|
+
// Wait for the automated fresh-session reply after the 5s timeout fires.
|
|
42
|
+
await chat.waitForMessage(
|
|
43
|
+
(m) =>
|
|
44
|
+
typeof m.content === 'string' &&
|
|
45
|
+
m.content.includes('[@clawmini/session-timeout] Starting a fresh session...'),
|
|
46
|
+
10000
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
await env.sendMessage('second message');
|
|
50
|
+
|
|
51
|
+
// The second message should use `commands.new` with an empty SESSION_ID.
|
|
52
|
+
const secondMsgLog = await chat.waitForMessage(
|
|
53
|
+
commandMatching((m) => m.stdout.includes('second message') && m.stdout.includes('[DEBUG'))
|
|
54
|
+
);
|
|
55
|
+
expect(secondMsgLog.stdout).toContain('[DEBUG NEW ]');
|
|
56
|
+
expect(secondMsgLog.stdout).not.toContain('[DEBUG APPEND ');
|
|
57
|
+
}, 20000);
|
|
58
|
+
|
|
59
|
+
it('/new before timeout: old session gets a background prompt, no user-facing fresh-session notice', async () => {
|
|
60
|
+
await env.addChat('test2');
|
|
61
|
+
chat = await env.connect('test2');
|
|
62
|
+
|
|
63
|
+
await env.sendMessage('msg A', { chat: 'test2', agent: 'test-agent' });
|
|
64
|
+
await env.sendMessage('/new', { chat: 'test2' });
|
|
65
|
+
await env.sendMessage('msg B', { chat: 'test2' });
|
|
66
|
+
|
|
67
|
+
// After 2s the msg A timer is at 2.5s, msg B timer is ~2s.
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
69
|
+
|
|
70
|
+
// Keep-alive resets the current session's timeout to 5s from now.
|
|
71
|
+
await env.sendMessage('msg KEEP ALIVE', { chat: 'test2' });
|
|
72
|
+
|
|
73
|
+
const keepAliveLog = await chat.waitForMessage(
|
|
74
|
+
commandMatching(
|
|
75
|
+
(m) => m.stdout.includes('msg KEEP ALIVE') && m.stdout.includes('[DEBUG APPEND')
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
const keepAliveSessionId = keepAliveLog.stdout.match(/\[DEBUG APPEND (.*?)\]/)?.[1];
|
|
79
|
+
expect(keepAliveSessionId).toBeTruthy();
|
|
80
|
+
|
|
81
|
+
// Wait another 3.5s: msg A is ~6s old (timer fired); keep-alive is ~3.5s (still fresh).
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 3500));
|
|
83
|
+
|
|
84
|
+
// The user should NOT be told the session was refreshed — /new already ended that session.
|
|
85
|
+
expect(
|
|
86
|
+
chat.messageBuffer.some(
|
|
87
|
+
(m) =>
|
|
88
|
+
typeof m.content === 'string' &&
|
|
89
|
+
m.content.includes('[@clawmini/session-timeout] Starting a fresh session...')
|
|
90
|
+
)
|
|
91
|
+
).toBe(false);
|
|
92
|
+
|
|
93
|
+
// But the background prompt SHOULD have fired for msg A's (already-ended) session.
|
|
94
|
+
const backgroundPrompts = chat.messageBuffer.filter(
|
|
95
|
+
(m) => typeof m.content === 'string' && m.content.includes('This chat session has ended.')
|
|
96
|
+
);
|
|
97
|
+
expect(backgroundPrompts.length).toBeGreaterThanOrEqual(1);
|
|
98
|
+
|
|
99
|
+
// Current session wasn't blown away by msg A's timeout.
|
|
100
|
+
await env.sendMessage('msg C', { chat: 'test2' });
|
|
101
|
+
const msgCLog = await chat.waitForMessage(
|
|
102
|
+
commandMatching((m) => m.stdout.includes('msg C') && m.stdout.includes('[DEBUG'))
|
|
103
|
+
);
|
|
104
|
+
expect(msgCLog.stdout).toContain(`[DEBUG APPEND ${keepAliveSessionId}]`);
|
|
105
|
+
}, 20000);
|
|
106
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, afterEach, beforeEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import {
|
|
4
|
+
TestEnvironment,
|
|
5
|
+
type ChatSubscription,
|
|
6
|
+
type ChatMessage,
|
|
7
|
+
} from '../_helpers/test-environment.js';
|
|
8
|
+
|
|
9
|
+
// Router replies surface as `system` messages with `displayRole: 'agent'`,
|
|
10
|
+
// produced by `logAutomaticReply` in chat-logger.ts.
|
|
11
|
+
function routerReplyContaining(text: string) {
|
|
12
|
+
return (m: ChatMessage): boolean =>
|
|
13
|
+
m.role === 'system' && typeof m.content === 'string' && m.content.includes(text);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('/model Router E2E', () => {
|
|
17
|
+
let env: TestEnvironment;
|
|
18
|
+
let chat: ChatSubscription | undefined;
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
env = new TestEnvironment('e2e-slash-model');
|
|
22
|
+
await env.setup();
|
|
23
|
+
await env.runCli(['init', '--agent', 'test-agent', '--agent-template', 'debug']);
|
|
24
|
+
await env.up();
|
|
25
|
+
await env.addChat('model-chat', 'test-agent');
|
|
26
|
+
}, 30000);
|
|
27
|
+
|
|
28
|
+
afterAll(() => env.teardown(), 30000);
|
|
29
|
+
afterEach(() => env.disconnectAll());
|
|
30
|
+
|
|
31
|
+
// Reset the overlay between tests so state from one test doesn't leak
|
|
32
|
+
// into the next. The daemon reads the overlay fresh on each /model call,
|
|
33
|
+
// so a direct file write is safe.
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
env.writeAgentSettings('test-agent', { extends: 'debug' });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('lists current model and shorthands on bare /model', async () => {
|
|
39
|
+
env.writeAgentSettings('test-agent', {
|
|
40
|
+
extends: 'debug',
|
|
41
|
+
env: { MODEL: 'gemini-3-pro' },
|
|
42
|
+
modelShorthands: { flash: 'gemini-3-flash-preview', pro: 'gemini-3-pro' },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
chat = await env.connect('model-chat');
|
|
46
|
+
await env.sendMessage('/model', { chat: 'model-chat' });
|
|
47
|
+
|
|
48
|
+
const msg = await chat.waitForMessage(routerReplyContaining('Current model: gemini-3-pro'));
|
|
49
|
+
expect(msg.content).toContain('- flash -> gemini-3-flash-preview');
|
|
50
|
+
expect(msg.content).toContain('- pro -> gemini-3-pro');
|
|
51
|
+
}, 15000);
|
|
52
|
+
|
|
53
|
+
it('sets env.MODEL to a literal model name and persists to overlay', async () => {
|
|
54
|
+
chat = await env.connect('model-chat');
|
|
55
|
+
await env.sendMessage('/model claude-opus-4-7', { chat: 'model-chat' });
|
|
56
|
+
|
|
57
|
+
await chat.waitForMessage(routerReplyContaining('Set MODEL to claude-opus-4-7'));
|
|
58
|
+
|
|
59
|
+
const overlay = env.getAgentSettings('test-agent') as { env?: { MODEL?: string } };
|
|
60
|
+
expect(overlay.env?.MODEL).toBe('claude-opus-4-7');
|
|
61
|
+
}, 15000);
|
|
62
|
+
|
|
63
|
+
it('resolves a shorthand and writes the full name to overlay', async () => {
|
|
64
|
+
env.writeAgentSettings('test-agent', {
|
|
65
|
+
extends: 'debug',
|
|
66
|
+
modelShorthands: { flash: 'gemini-3-flash-preview' },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
chat = await env.connect('model-chat');
|
|
70
|
+
await env.sendMessage('/model flash', { chat: 'model-chat' });
|
|
71
|
+
|
|
72
|
+
const msg = await chat.waitForMessage(
|
|
73
|
+
routerReplyContaining('Set MODEL to gemini-3-flash-preview')
|
|
74
|
+
);
|
|
75
|
+
expect(msg.content).toContain("shorthand 'flash'");
|
|
76
|
+
|
|
77
|
+
const overlay = env.getAgentSettings('test-agent') as { env?: { MODEL?: string } };
|
|
78
|
+
expect(overlay.env?.MODEL).toBe('gemini-3-flash-preview');
|
|
79
|
+
}, 15000);
|
|
80
|
+
|
|
81
|
+
it('round-trips /model add then /model <shorthand>', async () => {
|
|
82
|
+
chat = await env.connect('model-chat');
|
|
83
|
+
|
|
84
|
+
await env.sendMessage('/model add lite gemini-3.1-flash-lite', { chat: 'model-chat' });
|
|
85
|
+
await chat.waitForMessage(
|
|
86
|
+
routerReplyContaining('Added shorthand: lite -> gemini-3.1-flash-lite')
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const afterAdd = env.getAgentSettings('test-agent') as {
|
|
90
|
+
modelShorthands?: Record<string, string>;
|
|
91
|
+
};
|
|
92
|
+
expect(afterAdd.modelShorthands).toEqual({ lite: 'gemini-3.1-flash-lite' });
|
|
93
|
+
|
|
94
|
+
await env.sendMessage('/model lite', { chat: 'model-chat' });
|
|
95
|
+
await chat.waitForMessage(routerReplyContaining('Set MODEL to gemini-3.1-flash-lite'));
|
|
96
|
+
|
|
97
|
+
const afterUse = env.getAgentSettings('test-agent') as {
|
|
98
|
+
env?: { MODEL?: string };
|
|
99
|
+
modelShorthands?: Record<string, string>;
|
|
100
|
+
};
|
|
101
|
+
expect(afterUse.env?.MODEL).toBe('gemini-3.1-flash-lite');
|
|
102
|
+
expect(afterUse.modelShorthands).toEqual({ lite: 'gemini-3.1-flash-lite' });
|
|
103
|
+
}, 20000);
|
|
104
|
+
|
|
105
|
+
it('removes a shorthand from the overlay', async () => {
|
|
106
|
+
env.writeAgentSettings('test-agent', {
|
|
107
|
+
extends: 'debug',
|
|
108
|
+
modelShorthands: { flash: 'gemini-3-flash-preview', lite: 'gemini-3.1-flash-lite' },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
chat = await env.connect('model-chat');
|
|
112
|
+
await env.sendMessage('/model rm flash', { chat: 'model-chat' });
|
|
113
|
+
await chat.waitForMessage(routerReplyContaining('Removed shorthand: flash'));
|
|
114
|
+
|
|
115
|
+
const overlay = env.getAgentSettings('test-agent') as {
|
|
116
|
+
modelShorthands?: Record<string, string>;
|
|
117
|
+
};
|
|
118
|
+
expect(overlay.modelShorthands).toEqual({ lite: 'gemini-3.1-flash-lite' });
|
|
119
|
+
}, 15000);
|
|
120
|
+
|
|
121
|
+
// Regression: `/model add foo gemini-3 pro` previously stored the literal
|
|
122
|
+
// multi-token value `gemini-3 pro` as MODEL. Now rejects with usage.
|
|
123
|
+
it('rejects /model add when the full name has whitespace', async () => {
|
|
124
|
+
chat = await env.connect('model-chat');
|
|
125
|
+
await env.sendMessage('/model add foo gemini-3 pro', { chat: 'model-chat' });
|
|
126
|
+
|
|
127
|
+
await chat.waitForMessage(routerReplyContaining('Usage: /model add <shorthand> <full-name>'));
|
|
128
|
+
|
|
129
|
+
const overlay = env.getAgentSettings('test-agent') as {
|
|
130
|
+
modelShorthands?: Record<string, string>;
|
|
131
|
+
env?: { MODEL?: string };
|
|
132
|
+
};
|
|
133
|
+
expect(overlay.modelShorthands).toBeUndefined();
|
|
134
|
+
expect(overlay.env?.MODEL).toBeUndefined();
|
|
135
|
+
}, 15000);
|
|
136
|
+
|
|
137
|
+
// Regression: `/model X` against an agent without an overlay used to
|
|
138
|
+
// silently fabricate a stub `{env: {MODEL: X}}` settings file with no
|
|
139
|
+
// `extends`, breaking subsequent agent resolution. Now it refuses.
|
|
140
|
+
it('refuses to write when the agent has no settings overlay', async () => {
|
|
141
|
+
const settingsPath = env.getAgentPath('test-agent', 'settings.json');
|
|
142
|
+
fs.rmSync(settingsPath);
|
|
143
|
+
expect(fs.existsSync(settingsPath)).toBe(false);
|
|
144
|
+
|
|
145
|
+
chat = await env.connect('model-chat');
|
|
146
|
+
await env.sendMessage('/model gemini-3-pro', { chat: 'model-chat' });
|
|
147
|
+
|
|
148
|
+
await chat.waitForMessage(routerReplyContaining('has no settings overlay'));
|
|
149
|
+
|
|
150
|
+
expect(fs.existsSync(settingsPath)).toBe(false);
|
|
151
|
+
}, 15000);
|
|
152
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
2
|
+
import { TestEnvironment, type ChatSubscription, commandMatching } from '../_helpers/test-environment.js';
|
|
3
|
+
|
|
4
|
+
describe('/new Command E2E', () => {
|
|
5
|
+
let env: TestEnvironment;
|
|
6
|
+
let chat: ChatSubscription | undefined;
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
env = new TestEnvironment('e2e-slash-new');
|
|
10
|
+
await env.setup();
|
|
11
|
+
await env.runCli(['init', '--agent', 'test-agent', '--agent-template', 'debug']);
|
|
12
|
+
|
|
13
|
+
env.updateAgentSettings('test-agent', {
|
|
14
|
+
commands: {
|
|
15
|
+
new: 'echo "[DEBUG NEW $SESSION_ID] $CLAW_CLI_MESSAGE"',
|
|
16
|
+
append: 'echo "[DEBUG APPEND $SESSION_ID] $CLAW_CLI_MESSAGE"',
|
|
17
|
+
getSessionId: "node -e 'console.log(Math.random().toString(36).slice(2, 10))'",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
await env.up();
|
|
22
|
+
}, 30000);
|
|
23
|
+
|
|
24
|
+
afterAll(() => env.teardown(), 30000);
|
|
25
|
+
afterEach(() => env.disconnectAll());
|
|
26
|
+
|
|
27
|
+
it('resets the session ID when /new is sent', async () => {
|
|
28
|
+
chat = await env.connect('test-agent');
|
|
29
|
+
|
|
30
|
+
const { code } = await env.sendMessage('message 1');
|
|
31
|
+
expect(code).toBe(0);
|
|
32
|
+
|
|
33
|
+
await env.sendMessage('message 2');
|
|
34
|
+
|
|
35
|
+
const msg2Log = await chat.waitForMessage(
|
|
36
|
+
commandMatching((m) => m.stdout.includes('message 2') && m.stdout.includes('[DEBUG APPEND'))
|
|
37
|
+
);
|
|
38
|
+
const firstSessionId = msg2Log.stdout.match(/\[DEBUG APPEND (.*?)\]/)?.[1];
|
|
39
|
+
expect(firstSessionId).toBeTruthy();
|
|
40
|
+
|
|
41
|
+
await env.sendMessage('/new');
|
|
42
|
+
await env.sendMessage('message 3');
|
|
43
|
+
|
|
44
|
+
const msg3Log = await chat.waitForMessage(
|
|
45
|
+
commandMatching((m) => m.stdout.includes('message 3') && m.stdout.includes('[DEBUG'))
|
|
46
|
+
);
|
|
47
|
+
expect(msg3Log.stdout).toContain('[DEBUG NEW ]');
|
|
48
|
+
expect(msg3Log.stdout).not.toContain('[DEBUG APPEND');
|
|
49
|
+
}, 30000);
|
|
50
|
+
});
|