@otto-assistant/otto 0.1.2 → 0.7.16
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/bin.js +2 -0
- package/dist/agent-model.e2e.test.js +755 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/anthropic-account-identity.js +62 -0
- package/dist/anthropic-account-identity.test.js +38 -0
- package/dist/anthropic-auth-plugin.js +917 -0
- package/dist/anthropic-auth-state.js +303 -0
- package/dist/anthropic-auth-state.test.js +150 -0
- package/dist/bin.js +152 -0
- package/dist/btw-prefix-detection.js +17 -0
- package/dist/btw-prefix-detection.test.js +63 -0
- package/dist/channel-management.js +259 -0
- package/dist/cli-parsing.test.js +142 -0
- package/dist/cli-send-thread.e2e.test.js +353 -0
- package/dist/cli-telegram-options.test.js +99 -0
- package/dist/cli.js +4210 -568
- package/dist/commands/abort.js +65 -0
- package/dist/commands/action-buttons.js +245 -0
- package/dist/commands/add-dir.js +124 -0
- package/dist/commands/add-dir.test.js +126 -0
- package/dist/commands/add-project.js +113 -0
- package/dist/commands/agent.js +355 -0
- package/dist/commands/ask-question.js +320 -0
- package/dist/commands/ask-question.test.js +92 -0
- package/dist/commands/btw.js +121 -0
- package/dist/commands/cli-commands-group-a.test.js +728 -0
- package/dist/commands/cli-commands-group-b.test.js +695 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +130 -0
- package/dist/commands/diff.js +63 -0
- package/dist/commands/discord-commands-group-a.test.js +655 -0
- package/dist/commands/discord-commands-group-b.test.js +595 -0
- package/dist/commands/discord-commands-group-c.test.js +739 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork-subagent.js +177 -0
- package/dist/commands/fork.js +262 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +893 -0
- package/dist/commands/mcp.js +239 -0
- package/dist/commands/memory-snapshot.js +24 -0
- package/dist/commands/mention-mode.js +44 -0
- package/dist/commands/merge-worktree.js +162 -0
- package/dist/commands/model-variant.js +369 -0
- package/dist/commands/model.js +798 -0
- package/dist/commands/new-worktree.js +465 -0
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/permissions.js +274 -0
- package/dist/commands/queue.js +223 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +127 -0
- package/dist/commands/resume.js +149 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/screenshare.js +303 -0
- package/dist/commands/screenshare.test.js +20 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +176 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/tasks.js +205 -0
- package/dist/commands/thread-deletion-sync.js +50 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +305 -0
- package/dist/commands/unset-model.js +139 -0
- package/dist/commands/upgrade.js +48 -0
- package/dist/commands/user-command.js +155 -0
- package/dist/commands/verbosity.js +125 -0
- package/dist/commands/vscode.js +269 -0
- package/dist/commands/worktree-settings.js +43 -0
- package/dist/commands/worktrees.js +468 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +100 -255
- package/dist/context-awareness-plugin.js +340 -0
- package/dist/context-awareness-plugin.test.js +126 -0
- package/dist/critique-utils.js +95 -0
- package/dist/database.js +1355 -0
- package/dist/db.js +260 -0
- package/dist/db.test.js +138 -0
- package/dist/debounce-timeout.js +28 -0
- package/dist/debounced-process-flush.js +77 -0
- package/dist/discord-bot.js +1124 -0
- package/dist/discord-command-registration.js +567 -0
- package/dist/discord-urls.js +82 -0
- package/dist/discord-utils.js +616 -0
- package/dist/discord-utils.test.js +134 -0
- package/dist/errors.js +179 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/event-stream-real-capture.e2e.test.js +533 -0
- package/dist/eventsource-parser.test.js +327 -0
- package/dist/exec-async.js +26 -0
- package/dist/external-opencode-sync.js +480 -0
- package/dist/format-tables.js +491 -0
- package/dist/format-tables.test.js +478 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +113 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
- package/dist/gateway-proxy.e2e.test.js +485 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +37 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +58 -0
- package/dist/generated/internal/class.js +49 -0
- package/dist/generated/internal/prismaNamespace.js +254 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_events.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +122 -0
- package/dist/hrana-server.js +251 -0
- package/dist/hrana-server.test.js +370 -0
- package/dist/html-actions.js +123 -0
- package/dist/html-actions.test.js +70 -0
- package/dist/html-components.js +117 -0
- package/dist/html-components.test.js +34 -0
- package/dist/image-optimizer-plugin.js +153 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +420 -0
- package/dist/ipc-polling.js +327 -0
- package/dist/ipc-tools-plugin.js +193 -0
- package/dist/ipc-utils.js +18 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +171 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +264 -0
- package/dist/memory-overview-plugin.js +128 -0
- package/dist/message-finish-field.e2e.test.js +168 -0
- package/dist/message-formatting.js +415 -0
- package/dist/message-formatting.test.js +115 -0
- package/dist/message-preprocessing.js +359 -0
- package/dist/onboarding-tutorial.js +163 -0
- package/dist/onboarding-welcome.js +37 -0
- package/dist/openai-realtime.js +224 -0
- package/dist/opencode-command-detection.js +65 -0
- package/dist/opencode-command-detection.test.js +240 -0
- package/dist/opencode-command.js +131 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +388 -0
- package/dist/opencode-interrupt-plugin.test.js +463 -0
- package/dist/opencode.js +1124 -0
- package/dist/otto/branding.js +22 -0
- package/dist/otto/index.js +21 -0
- package/dist/otto-digital-twin.e2e.test.js +161 -0
- package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
- package/dist/otto-opencode-plugin.js +21 -0
- package/dist/otto-opencode-plugin.test.js +98 -0
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/patch-text-parser.js +97 -0
- package/dist/plugin-logger.js +68 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/queue-advanced-abort.e2e.test.js +293 -0
- package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
- package/dist/queue-advanced-e2e-setup.js +790 -0
- package/dist/queue-advanced-footer.e2e.test.js +481 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
- package/dist/queue-advanced-question.e2e.test.js +261 -0
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
- package/dist/queue-advanced-typing.e2e.test.js +153 -0
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
- package/dist/queue-interrupt-drain.e2e.test.js +135 -0
- package/dist/queue-question-select-drain.e2e.test.js +256 -0
- package/dist/runtime-idle-sweeper.js +52 -0
- package/dist/runtime-lifecycle.e2e.test.js +514 -0
- package/dist/sentry.js +23 -0
- package/dist/session-handler/agent-utils.js +67 -0
- package/dist/session-handler/event-stream-state.js +475 -0
- package/dist/session-handler/event-stream-state.test.js +632 -0
- package/dist/session-handler/model-utils.js +147 -0
- package/dist/session-handler/opencode-session-event-log.js +94 -0
- package/dist/session-handler/thread-runtime-state.js +131 -0
- package/dist/session-handler/thread-session-runtime.js +3390 -0
- package/dist/session-handler.js +9 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/session-title-rename.test.js +92 -0
- package/dist/skill-filter.js +31 -0
- package/dist/skill-filter.test.js +65 -0
- package/dist/startup-service.js +153 -0
- package/dist/startup-time.e2e.test.js +296 -0
- package/dist/store.js +19 -0
- package/dist/subagent-rate-limit-plugin.js +175 -0
- package/dist/system-message.js +702 -0
- package/dist/system-message.test.js +697 -0
- package/dist/task-runner.js +530 -0
- package/dist/task-schedule.js +213 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/test-utils.js +313 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +1111 -0
- package/dist/tools.js +357 -0
- package/dist/undo-redo.e2e.test.js +161 -0
- package/dist/unnest-code-blocks.js +146 -0
- package/dist/unnest-code-blocks.test.js +673 -0
- package/dist/upgrade.js +156 -0
- package/dist/utils.js +172 -0
- package/dist/utils.test.js +130 -0
- package/dist/voice-attachment.js +34 -0
- package/dist/voice-handler.js +646 -0
- package/dist/voice-message.e2e.test.js +1021 -0
- package/dist/voice.js +456 -0
- package/dist/voice.test.js +235 -0
- package/dist/wait-session.js +171 -0
- package/dist/websockify.js +69 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-lifecycle.e2e.test.js +311 -0
- package/dist/worktree-utils.js +3 -0
- package/dist/worktrees.js +991 -0
- package/dist/worktrees.test.js +415 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +90 -38
- package/schema.prisma +303 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +112 -0
- package/skills/egaki/SKILL.md +100 -0
- package/skills/errore/SKILL.md +647 -0
- package/skills/event-sourcing-state/SKILL.md +252 -0
- package/skills/goke/SKILL.md +38 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/lintcn/SKILL.md +873 -0
- package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
- package/skills/new-skill/SKILL.md +237 -0
- package/skills/npm-package/SKILL.md +617 -0
- package/skills/opensrc/SKILL.md +78 -0
- package/skills/otto-publish/SKILL.md +61 -0
- package/skills/playwriter/SKILL.md +35 -0
- package/skills/profano/SKILL.md +16 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/sigillo/SKILL.md +101 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/spiceflow/SKILL.md +28 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +98 -0
- package/skills/usecomputer/SKILL.md +264 -0
- package/skills/x-articles/SKILL.md +554 -0
- package/skills/zele/SKILL.md +49 -0
- package/skills/zustand-centralized-state/SKILL.md +1004 -0
- package/src/agent-model.e2e.test.ts +979 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +283 -0
- package/src/ai-tool.ts +39 -0
- package/src/anthropic-account-identity.test.ts +52 -0
- package/src/anthropic-account-identity.ts +77 -0
- package/src/anthropic-auth-plugin.ts +1139 -0
- package/src/anthropic-auth-state.test.ts +187 -0
- package/src/anthropic-auth-state.ts +386 -0
- package/src/bin.ts +182 -0
- package/src/btw-prefix-detection.test.ts +73 -0
- package/src/btw-prefix-detection.ts +23 -0
- package/src/channel-management.ts +376 -0
- package/src/cli-parsing.test.ts +197 -0
- package/src/cli-send-thread.e2e.test.ts +463 -0
- package/src/cli-telegram-options.test.ts +114 -0
- package/src/cli.ts +5718 -580
- package/src/commands/abort.ts +89 -0
- package/src/commands/action-buttons.ts +364 -0
- package/src/commands/add-dir.test.ts +154 -0
- package/src/commands/add-dir.ts +175 -0
- package/src/commands/add-project.ts +149 -0
- package/src/commands/agent.ts +496 -0
- package/src/commands/ask-question.test.ts +111 -0
- package/src/commands/ask-question.ts +455 -0
- package/src/commands/btw.ts +184 -0
- package/src/commands/cli-commands-group-a.test.ts +837 -0
- package/src/commands/cli-commands-group-b.test.ts +800 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +190 -0
- package/src/commands/diff.ts +91 -0
- package/src/commands/discord-commands-group-a.test.ts +789 -0
- package/src/commands/discord-commands-group-b.test.ts +648 -0
- package/src/commands/discord-commands-group-c.test.ts +882 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork-subagent.ts +263 -0
- package/src/commands/fork.ts +386 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +1181 -0
- package/src/commands/mcp.ts +307 -0
- package/src/commands/memory-snapshot.ts +30 -0
- package/src/commands/mention-mode.ts +68 -0
- package/src/commands/merge-worktree.ts +226 -0
- package/src/commands/model-variant.ts +488 -0
- package/src/commands/model.ts +1082 -0
- package/src/commands/new-worktree.ts +645 -0
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/permissions.ts +397 -0
- package/src/commands/queue.ts +293 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +230 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/screenshare.test.ts +30 -0
- package/src/commands/screenshare.ts +366 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +227 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/tasks.ts +293 -0
- package/src/commands/thread-deletion-sync.ts +80 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +386 -0
- package/src/commands/unset-model.ts +174 -0
- package/src/commands/upgrade.ts +59 -0
- package/src/commands/user-command.ts +198 -0
- package/src/commands/verbosity.ts +173 -0
- package/src/commands/vscode.ts +342 -0
- package/src/commands/worktree-settings.ts +70 -0
- package/src/commands/worktrees.ts +645 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +103 -339
- package/src/context-awareness-plugin.test.ts +144 -0
- package/src/context-awareness-plugin.ts +469 -0
- package/src/critique-utils.ts +139 -0
- package/src/database.ts +1949 -0
- package/src/db.test.ts +162 -0
- package/src/db.ts +295 -0
- package/src/debounce-timeout.ts +43 -0
- package/src/debounced-process-flush.ts +104 -0
- package/src/discord-bot.ts +1507 -0
- package/src/discord-command-registration.ts +752 -0
- package/src/discord-urls.ts +89 -0
- package/src/discord-utils.test.ts +153 -0
- package/src/discord-utils.ts +846 -0
- package/src/errors.ts +232 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/event-stream-real-capture.e2e.test.ts +692 -0
- package/src/eventsource-parser.test.ts +351 -0
- package/src/exec-async.ts +35 -0
- package/src/external-opencode-sync.ts +685 -0
- package/src/format-tables.test.ts +515 -0
- package/src/format-tables.ts +718 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +172 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
- package/src/gateway-proxy.e2e.test.ts +644 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +114 -0
- package/src/generated/client.ts +138 -0
- package/src/generated/commonInputTypes.ts +770 -0
- package/src/generated/enums.ts +98 -0
- package/src/generated/internal/class.ts +384 -0
- package/src/generated/internal/prismaNamespace.ts +2394 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1700 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +1859 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1228 -0
- package/src/generated/models/channel_worktrees.ts +1300 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_events.ts +1439 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1781 -0
- package/src/generated/models/thread_worktrees.ts +1356 -0
- package/src/generated/models.ts +30 -0
- package/src/heap-monitor.ts +152 -0
- package/src/hrana-server.test.ts +434 -0
- package/src/hrana-server.ts +299 -0
- package/src/html-actions.test.ts +87 -0
- package/src/html-actions.ts +174 -0
- package/src/html-components.test.ts +38 -0
- package/src/html-components.ts +181 -0
- package/src/image-optimizer-plugin.ts +194 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +610 -0
- package/src/ipc-polling.ts +427 -0
- package/src/ipc-tools-plugin.ts +236 -0
- package/src/ipc-utils.ts +29 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +215 -0
- package/src/markdown.test.ts +315 -0
- package/src/markdown.ts +410 -0
- package/src/memory-overview-plugin.ts +163 -0
- package/src/message-finish-field.e2e.test.ts +195 -0
- package/src/message-formatting.test.ts +126 -0
- package/src/message-formatting.ts +535 -0
- package/src/message-preprocessing.ts +488 -0
- package/src/onboarding-tutorial.ts +167 -0
- package/src/onboarding-welcome.ts +49 -0
- package/src/openai-realtime.ts +358 -0
- package/src/opencode-command-detection.test.ts +307 -0
- package/src/opencode-command-detection.ts +76 -0
- package/src/opencode-command.test.ts +70 -0
- package/src/opencode-command.ts +191 -0
- package/src/opencode-interrupt-plugin.test.ts +682 -0
- package/src/opencode-interrupt-plugin.ts +507 -0
- package/src/opencode.ts +1462 -0
- package/src/otto/branding.ts +23 -0
- package/src/otto/index.ts +22 -0
- package/src/otto-digital-twin.e2e.test.ts +199 -0
- package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
- package/src/otto-opencode-plugin.test.ts +108 -0
- package/src/otto-opencode-plugin.ts +22 -0
- package/src/parse-permission-rules.test.ts +127 -0
- package/src/patch-text-parser.ts +107 -0
- package/src/plugin-logger.ts +84 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/queue-advanced-abort.e2e.test.ts +382 -0
- package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
- package/src/queue-advanced-e2e-setup.ts +877 -0
- package/src/queue-advanced-footer.e2e.test.ts +591 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
- package/src/queue-advanced-question.e2e.test.ts +316 -0
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
- package/src/queue-advanced-typing.e2e.test.ts +199 -0
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
- package/src/queue-interrupt-drain.e2e.test.ts +166 -0
- package/src/queue-question-select-drain.e2e.test.ts +327 -0
- package/src/runtime-idle-sweeper.ts +76 -0
- package/src/runtime-lifecycle.e2e.test.ts +651 -0
- package/src/schema.sql +174 -0
- package/src/sentry.ts +26 -0
- package/src/session-handler/agent-utils.ts +99 -0
- package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
- package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
- package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
- package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
- package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
- package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
- package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
- package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
- package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
- package/src/session-handler/event-stream-state.test.ts +717 -0
- package/src/session-handler/event-stream-state.ts +706 -0
- package/src/session-handler/model-utils.ts +217 -0
- package/src/session-handler/opencode-session-event-log.ts +130 -0
- package/src/session-handler/thread-runtime-state.ts +247 -0
- package/src/session-handler/thread-session-runtime.ts +4440 -0
- package/src/session-handler.ts +15 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/session-title-rename.test.ts +130 -0
- package/src/skill-filter.test.ts +83 -0
- package/src/skill-filter.ts +42 -0
- package/src/startup-service.ts +200 -0
- package/src/startup-time.e2e.test.ts +373 -0
- package/src/store.ts +139 -0
- package/src/subagent-rate-limit-plugin.ts +218 -0
- package/src/system-message.test.ts +710 -0
- package/src/system-message.ts +814 -0
- package/src/task-runner.ts +725 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +317 -0
- package/src/test-utils.ts +451 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +1350 -0
- package/src/tools.ts +430 -0
- package/src/undici.d.ts +12 -0
- package/src/undo-redo.e2e.test.ts +209 -0
- package/src/unnest-code-blocks.test.ts +713 -0
- package/src/unnest-code-blocks.ts +185 -0
- package/src/upgrade.ts +185 -0
- package/src/utils.test.ts +155 -0
- package/src/utils.ts +265 -0
- package/src/voice-attachment.ts +51 -0
- package/src/voice-handler.ts +908 -0
- package/src/voice-message.e2e.test.ts +1255 -0
- package/src/voice.test.ts +281 -0
- package/src/voice.ts +638 -0
- package/src/wait-session.ts +273 -0
- package/src/websockify.ts +101 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-lifecycle.e2e.test.ts +396 -0
- package/src/worktree-utils.ts +4 -0
- package/src/worktrees.test.ts +489 -0
- package/src/worktrees.ts +1370 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
- package/README.md +0 -142
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config.d.ts +0 -39
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -202
- package/dist/config.test.js.map +0 -1
- package/dist/detect.d.ts +0 -9
- package/dist/detect.d.ts.map +0 -1
- package/dist/detect.js +0 -40
- package/dist/detect.js.map +0 -1
- package/dist/detect.test.d.ts +0 -2
- package/dist/detect.test.d.ts.map +0 -1
- package/dist/detect.test.js +0 -26
- package/dist/detect.test.js.map +0 -1
- package/dist/docker.d.ts +0 -7
- package/dist/docker.d.ts.map +0 -1
- package/dist/docker.js +0 -17
- package/dist/docker.js.map +0 -1
- package/dist/docker.test.d.ts +0 -2
- package/dist/docker.test.d.ts.map +0 -1
- package/dist/docker.test.js +0 -12
- package/dist/docker.test.js.map +0 -1
- package/dist/health.d.ts +0 -31
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -117
- package/dist/health.js.map +0 -1
- package/dist/health.test.d.ts +0 -2
- package/dist/health.test.d.ts.map +0 -1
- package/dist/health.test.js +0 -52
- package/dist/health.test.js.map +0 -1
- package/dist/index.d.ts +0 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -15
- package/dist/index.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -8
- package/dist/index.test.js.map +0 -1
- package/dist/installer.d.ts +0 -10
- package/dist/installer.d.ts.map +0 -1
- package/dist/installer.js +0 -50
- package/dist/installer.js.map +0 -1
- package/dist/installer.test.d.ts +0 -2
- package/dist/installer.test.d.ts.map +0 -1
- package/dist/installer.test.js +0 -43
- package/dist/installer.test.js.map +0 -1
- package/dist/lifecycle.d.ts +0 -10
- package/dist/lifecycle.d.ts.map +0 -1
- package/dist/lifecycle.js +0 -45
- package/dist/lifecycle.js.map +0 -1
- package/dist/lifecycle.test.d.ts +0 -2
- package/dist/lifecycle.test.d.ts.map +0 -1
- package/dist/lifecycle.test.js +0 -20
- package/dist/lifecycle.test.js.map +0 -1
- package/dist/manifest.d.ts +0 -18
- package/dist/manifest.d.ts.map +0 -1
- package/dist/manifest.js +0 -30
- package/dist/manifest.js.map +0 -1
- package/dist/skills-baseline.d.ts +0 -7
- package/dist/skills-baseline.d.ts.map +0 -1
- package/dist/skills-baseline.js +0 -9
- package/dist/skills-baseline.js.map +0 -1
- package/dist/skills.d.ts +0 -110
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js +0 -429
- package/dist/skills.js.map +0 -1
- package/dist/skills.test.d.ts +0 -2
- package/dist/skills.test.d.ts.map +0 -1
- package/dist/skills.test.js +0 -416
- package/dist/skills.test.js.map +0 -1
- package/dist/sync.d.ts +0 -10
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -39
- package/dist/sync.js.map +0 -1
- package/dist/tenant.d.ts +0 -13
- package/dist/tenant.d.ts.map +0 -1
- package/dist/tenant.js +0 -105
- package/dist/tenant.js.map +0 -1
- package/dist/tenant.test.d.ts +0 -2
- package/dist/tenant.test.d.ts.map +0 -1
- package/dist/tenant.test.js +0 -37
- package/dist/tenant.test.js.map +0 -1
- package/src/config.test.ts +0 -237
- package/src/detect.test.ts +0 -29
- package/src/detect.ts +0 -52
- package/src/docker.test.ts +0 -12
- package/src/docker.ts +0 -23
- package/src/health.test.ts +0 -61
- package/src/health.ts +0 -158
- package/src/index.test.ts +0 -8
- package/src/index.ts +0 -62
- package/src/installer.test.ts +0 -52
- package/src/installer.ts +0 -62
- package/src/lifecycle.test.ts +0 -23
- package/src/lifecycle.ts +0 -49
- package/src/manifest.ts +0 -42
- package/src/skills-baseline.ts +0 -14
- package/src/skills.test.ts +0 -503
- package/src/skills.ts +0 -512
- package/src/sync.ts +0 -53
- package/src/tenant.test.ts +0 -49
- package/src/tenant.ts +0 -120
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// E2e test: queued messages must drain immediately when the session is idle,
|
|
2
|
+
// even if action buttons are still pending. The isSessionBusy check is
|
|
3
|
+
// sufficient — hasPendingInteractiveUi() should NOT block queue drain.
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect } from 'vitest'
|
|
6
|
+
import {
|
|
7
|
+
setupQueueAdvancedSuite,
|
|
8
|
+
TEST_USER_ID,
|
|
9
|
+
} from './queue-advanced-e2e-setup.js'
|
|
10
|
+
import {
|
|
11
|
+
waitForBotMessageContaining,
|
|
12
|
+
waitForFooterMessage,
|
|
13
|
+
} from './test-utils.js'
|
|
14
|
+
import { getThreadSession } from './database.js'
|
|
15
|
+
import {
|
|
16
|
+
pendingActionButtonContexts,
|
|
17
|
+
showActionButtons,
|
|
18
|
+
} from './commands/action-buttons.js'
|
|
19
|
+
|
|
20
|
+
const TEXT_CHANNEL_ID = '200000000000001020'
|
|
21
|
+
|
|
22
|
+
describe('queue drain with pending interactive UI', () => {
|
|
23
|
+
const ctx = setupQueueAdvancedSuite({
|
|
24
|
+
channelId: TEXT_CHANNEL_ID,
|
|
25
|
+
channelName: 'qa-drain-interactive-ui',
|
|
26
|
+
dirName: 'qa-drain-interactive-ui',
|
|
27
|
+
username: 'drain-ui-tester',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test(
|
|
31
|
+
'queued message drains immediately while action buttons are still pending',
|
|
32
|
+
async () => {
|
|
33
|
+
// 1. Create a thread with a first completed reply
|
|
34
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
35
|
+
content: 'Reply with exactly: drain-button-setup',
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
39
|
+
timeout: 4_000,
|
|
40
|
+
predicate: (t) => {
|
|
41
|
+
return t.name === 'Reply with exactly: drain-button-setup'
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const th = ctx.discord.thread(thread.id)
|
|
46
|
+
|
|
47
|
+
await waitForBotMessageContaining({
|
|
48
|
+
discord: ctx.discord,
|
|
49
|
+
threadId: thread.id,
|
|
50
|
+
userId: TEST_USER_ID,
|
|
51
|
+
text: 'ok',
|
|
52
|
+
timeout: 4_000,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
await waitForFooterMessage({
|
|
56
|
+
discord: ctx.discord,
|
|
57
|
+
threadId: thread.id,
|
|
58
|
+
timeout: 4_000,
|
|
59
|
+
afterMessageIncludes: 'ok',
|
|
60
|
+
afterAuthorId: ctx.discord.botUserId,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// 2. Show action buttons (session is idle, buttons are pending)
|
|
64
|
+
const currentSessionId = await getThreadSession(thread.id)
|
|
65
|
+
if (!currentSessionId) {
|
|
66
|
+
throw new Error('Expected thread session id')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const channel = await ctx.botClient.channels.fetch(thread.id)
|
|
70
|
+
if (!channel || !channel.isThread()) {
|
|
71
|
+
throw new Error('Expected Discord thread channel')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await showActionButtons({
|
|
75
|
+
thread: channel,
|
|
76
|
+
sessionId: currentSessionId,
|
|
77
|
+
directory: ctx.directories.projectDirectory,
|
|
78
|
+
buttons: [{ label: 'Pending button', color: 'white' }],
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Verify buttons are pending
|
|
82
|
+
const start = Date.now()
|
|
83
|
+
while (Date.now() - start < 4_000) {
|
|
84
|
+
const entry = [...pendingActionButtonContexts.entries()].find(([, context]) => {
|
|
85
|
+
return context.thread.id === thread.id && Boolean(context.messageId)
|
|
86
|
+
})
|
|
87
|
+
if (entry) {
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
await new Promise<void>((resolve) => {
|
|
91
|
+
setTimeout(resolve, 100)
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
expect(
|
|
95
|
+
[...pendingActionButtonContexts.values()].some((c) => {
|
|
96
|
+
return c.thread.id === thread.id
|
|
97
|
+
}),
|
|
98
|
+
).toBe(true)
|
|
99
|
+
|
|
100
|
+
// 3. Queue a message via /queue while buttons are still pending.
|
|
101
|
+
// The queue should drain immediately because session is idle.
|
|
102
|
+
// Currently FAILS: hasPendingInteractiveUi() blocks tryDrainQueue().
|
|
103
|
+
const { id: queueInteractionId } = await th.user(TEST_USER_ID)
|
|
104
|
+
.runSlashCommand({
|
|
105
|
+
name: 'queue',
|
|
106
|
+
options: [{ name: 'message', type: 3, value: 'Reply with exactly: post-button-drain' }],
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const queueAck = await th.waitForInteractionAck({
|
|
110
|
+
interactionId: queueInteractionId,
|
|
111
|
+
timeout: 4_000,
|
|
112
|
+
})
|
|
113
|
+
if (!queueAck.messageId) {
|
|
114
|
+
throw new Error('Expected /queue response message id')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 4. Queued message should dispatch immediately (not stay "Queued").
|
|
118
|
+
// The dispatch indicator should appear quickly.
|
|
119
|
+
await waitForBotMessageContaining({
|
|
120
|
+
discord: ctx.discord,
|
|
121
|
+
threadId: thread.id,
|
|
122
|
+
text: '» **drain-ui-tester:** Reply with exactly: post-button-drain',
|
|
123
|
+
timeout: 4_000,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// 5. Wait for the footer after the drained message completes
|
|
127
|
+
await waitForFooterMessage({
|
|
128
|
+
discord: ctx.discord,
|
|
129
|
+
threadId: thread.id,
|
|
130
|
+
timeout: 4_000,
|
|
131
|
+
afterMessageIncludes: '» **drain-ui-tester:**',
|
|
132
|
+
afterAuthorId: ctx.discord.botUserId,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const timeline = await th.text({ showInteractions: true })
|
|
136
|
+
expect(timeline).toMatchInlineSnapshot(`
|
|
137
|
+
"--- from: user (drain-ui-tester)
|
|
138
|
+
Reply with exactly: drain-button-setup
|
|
139
|
+
--- from: assistant (TestBot)
|
|
140
|
+
⬥ ok
|
|
141
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
142
|
+
**Action Required**
|
|
143
|
+
[user interaction]
|
|
144
|
+
» **drain-ui-tester:** Reply with exactly: post-button-drain
|
|
145
|
+
⬥ ok
|
|
146
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
147
|
+
`)
|
|
148
|
+
},
|
|
149
|
+
20_000,
|
|
150
|
+
)
|
|
151
|
+
})
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// E2e test for queue + interrupt interaction.
|
|
2
|
+
// Validates that a user can queue a command via /queue while a slow session
|
|
3
|
+
// is in progress, then send a normal (non-queued) message to interrupt.
|
|
4
|
+
//
|
|
5
|
+
// Expected behavior:
|
|
6
|
+
// 1. Slow session is running
|
|
7
|
+
// 2. User queues a message via /queue (enters otto local queue)
|
|
8
|
+
// 3. User sends a normal message (interrupt)
|
|
9
|
+
// 4. Session aborts the slow task, processes the interrupt message immediately
|
|
10
|
+
// 5. Interrupt response appears in Discord with a ⬥ ok reply
|
|
11
|
+
// 6. When interrupt response completes, the queued message drains and runs
|
|
12
|
+
//
|
|
13
|
+
// Uses opencode-deterministic-provider (no real LLM calls).
|
|
14
|
+
// Poll timeouts: 4s max, 100ms interval. Slow matcher uses 100s delay.
|
|
15
|
+
|
|
16
|
+
import { describe, test, expect } from 'vitest'
|
|
17
|
+
import {
|
|
18
|
+
setupQueueAdvancedSuite,
|
|
19
|
+
TEST_USER_ID,
|
|
20
|
+
} from './queue-advanced-e2e-setup.js'
|
|
21
|
+
import {
|
|
22
|
+
waitForFooterMessage,
|
|
23
|
+
waitForBotMessageContaining,
|
|
24
|
+
waitForMessageById,
|
|
25
|
+
} from './test-utils.js'
|
|
26
|
+
|
|
27
|
+
const TEXT_CHANNEL_ID = '200000000000001099'
|
|
28
|
+
|
|
29
|
+
const e2eTest = describe
|
|
30
|
+
|
|
31
|
+
e2eTest('queue + interrupt drain ordering', () => {
|
|
32
|
+
const ctx = setupQueueAdvancedSuite({
|
|
33
|
+
channelId: TEXT_CHANNEL_ID,
|
|
34
|
+
channelName: 'qa-interrupt-drain-e2e',
|
|
35
|
+
dirName: 'qa-interrupt-drain-e2e',
|
|
36
|
+
username: 'interrupt-tester',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test(
|
|
40
|
+
'queued message via /queue + normal interrupt: interrupt reply should appear, then queue drains',
|
|
41
|
+
async () => {
|
|
42
|
+
// 1. Establish session with a quick first message
|
|
43
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
44
|
+
content: 'Reply with exactly: setup-interrupt-drain',
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
48
|
+
timeout: 4_000,
|
|
49
|
+
predicate: (t) => {
|
|
50
|
+
return t.name === 'Reply with exactly: setup-interrupt-drain'
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const th = ctx.discord.thread(thread.id)
|
|
55
|
+
await th.waitForBotReply({ timeout: 4_000 })
|
|
56
|
+
|
|
57
|
+
// Wait for first run to fully complete (footer) so state is clean
|
|
58
|
+
await waitForFooterMessage({
|
|
59
|
+
discord: ctx.discord,
|
|
60
|
+
threadId: thread.id,
|
|
61
|
+
timeout: 4_000,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// 2. Start a slow session — PLUGIN_TIMEOUT_SLEEP_MARKER has a 100s delay
|
|
65
|
+
// before the finish event, guaranteeing the session stays busy.
|
|
66
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
67
|
+
content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Wait for the slow matcher to start streaming (text appears before delay)
|
|
71
|
+
await waitForBotMessageContaining({
|
|
72
|
+
discord: ctx.discord,
|
|
73
|
+
threadId: thread.id,
|
|
74
|
+
userId: TEST_USER_ID,
|
|
75
|
+
text: 'starting sleep',
|
|
76
|
+
afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
|
|
77
|
+
timeout: 4_000,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// 3. Queue a message via /queue while the slow session is running
|
|
81
|
+
const { id: queueInteractionId } = await th.user(TEST_USER_ID)
|
|
82
|
+
.runSlashCommand({
|
|
83
|
+
name: 'queue',
|
|
84
|
+
options: [{ name: 'message', type: 3, value: 'Reply with exactly: queued-behind-slow' }],
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const queueAck = await th.waitForInteractionAck({
|
|
88
|
+
interactionId: queueInteractionId,
|
|
89
|
+
timeout: 4_000,
|
|
90
|
+
})
|
|
91
|
+
if (!queueAck.messageId) {
|
|
92
|
+
throw new Error('Expected /queue response message id')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const queueStatusMessage = await waitForMessageById({
|
|
96
|
+
discord: ctx.discord,
|
|
97
|
+
threadId: thread.id,
|
|
98
|
+
messageId: queueAck.messageId,
|
|
99
|
+
timeout: 4_000,
|
|
100
|
+
})
|
|
101
|
+
// The /queue message should be queued (session is busy with the 100s task)
|
|
102
|
+
expect(queueStatusMessage.content).toContain('Queued message')
|
|
103
|
+
|
|
104
|
+
// 4. Send a normal (non-queued) message — this should interrupt the slow
|
|
105
|
+
// session and be processed immediately
|
|
106
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
107
|
+
content: 'Reply with exactly: interrupt-now',
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// 5. Wait for the final state: the interrupt message should get its own
|
|
111
|
+
// ⬥ ok reply, then the queued message should drain and get processed.
|
|
112
|
+
// We wait for the queued message's footer as the final signal.
|
|
113
|
+
await waitForFooterMessage({
|
|
114
|
+
discord: ctx.discord,
|
|
115
|
+
threadId: thread.id,
|
|
116
|
+
timeout: 12_000,
|
|
117
|
+
afterMessageIncludes: 'queued-behind-slow',
|
|
118
|
+
afterAuthorId: ctx.discord.botUserId,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// 6. Capture the full interaction in an inline snapshot.
|
|
122
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
123
|
+
"--- from: user (interrupt-tester)
|
|
124
|
+
Reply with exactly: setup-interrupt-drain
|
|
125
|
+
--- from: assistant (TestBot)
|
|
126
|
+
⬥ ok
|
|
127
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
128
|
+
--- from: user (interrupt-tester)
|
|
129
|
+
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
130
|
+
--- from: assistant (TestBot)
|
|
131
|
+
⬥ starting sleep 100
|
|
132
|
+
Queued message (position 1)
|
|
133
|
+
--- from: user (interrupt-tester)
|
|
134
|
+
Reply with exactly: interrupt-now
|
|
135
|
+
--- from: assistant (TestBot)
|
|
136
|
+
⬥ ok
|
|
137
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
138
|
+
» **interrupt-tester:** Reply with exactly: queued-behind-slow
|
|
139
|
+
⬥ ok
|
|
140
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
141
|
+
`)
|
|
142
|
+
|
|
143
|
+
// 7. Assert the interrupt message got its own ⬥ ok reply between the
|
|
144
|
+
// user's interrupt message and the queue dispatch indicator.
|
|
145
|
+
const text = await th.text()
|
|
146
|
+
const lines = text.split('\n')
|
|
147
|
+
|
|
148
|
+
const interruptUserLine = lines.findIndex((line) => {
|
|
149
|
+
return line.includes('Reply with exactly: interrupt-now')
|
|
150
|
+
})
|
|
151
|
+
expect(interruptUserLine).toBeGreaterThan(-1)
|
|
152
|
+
|
|
153
|
+
const queueDispatchLine = lines.findIndex((line) => {
|
|
154
|
+
return line.includes('» **interrupt-tester:** Reply with exactly: queued-behind-slow')
|
|
155
|
+
})
|
|
156
|
+
expect(queueDispatchLine).toBeGreaterThan(-1)
|
|
157
|
+
|
|
158
|
+
const linesBetween = lines.slice(interruptUserLine + 1, queueDispatchLine)
|
|
159
|
+
const hasInterruptReply = linesBetween.some((line) => {
|
|
160
|
+
return line.includes('⬥ ok')
|
|
161
|
+
})
|
|
162
|
+
expect(hasInterruptReply).toBe(true)
|
|
163
|
+
},
|
|
164
|
+
20_000,
|
|
165
|
+
)
|
|
166
|
+
})
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// E2e test: queued message must drain after the user answers a pending question
|
|
2
|
+
// via the Discord dropdown select menu. Reproduces a bug where answering via
|
|
3
|
+
// select (not text) leaves queued messages stuck because the session continues
|
|
4
|
+
// processing after the answer and may enter another blocking state.
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect } from 'vitest'
|
|
7
|
+
import {
|
|
8
|
+
setupQueueAdvancedSuite,
|
|
9
|
+
TEST_USER_ID,
|
|
10
|
+
} from './queue-advanced-e2e-setup.js'
|
|
11
|
+
import {
|
|
12
|
+
waitForBotMessageContaining,
|
|
13
|
+
waitForFooterMessage,
|
|
14
|
+
} from './test-utils.js'
|
|
15
|
+
import { pendingQuestionContexts } from './commands/ask-question.js'
|
|
16
|
+
|
|
17
|
+
const TEXT_CHANNEL_ID = '200000000000001030'
|
|
18
|
+
|
|
19
|
+
async function waitForPendingQuestion({
|
|
20
|
+
threadId,
|
|
21
|
+
timeoutMs,
|
|
22
|
+
}: {
|
|
23
|
+
threadId: string
|
|
24
|
+
timeoutMs: number
|
|
25
|
+
}): Promise<{ contextHash: string }> {
|
|
26
|
+
const start = Date.now()
|
|
27
|
+
while (Date.now() - start < timeoutMs) {
|
|
28
|
+
const entry = [...pendingQuestionContexts.entries()].find(([, context]) => {
|
|
29
|
+
return context.thread.id === threadId
|
|
30
|
+
})
|
|
31
|
+
if (entry) {
|
|
32
|
+
return { contextHash: entry[0] }
|
|
33
|
+
}
|
|
34
|
+
await new Promise<void>((resolve) => {
|
|
35
|
+
setTimeout(resolve, 100)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
throw new Error('Timed out waiting for pending question context')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function expectNoBotMessageContaining({
|
|
42
|
+
discord,
|
|
43
|
+
threadId,
|
|
44
|
+
text,
|
|
45
|
+
timeout,
|
|
46
|
+
}: {
|
|
47
|
+
discord: Parameters<typeof waitForBotMessageContaining>[0]['discord']
|
|
48
|
+
threadId: string
|
|
49
|
+
text: string
|
|
50
|
+
timeout: number
|
|
51
|
+
}): Promise<void> {
|
|
52
|
+
const start = Date.now()
|
|
53
|
+
while (Date.now() - start < timeout) {
|
|
54
|
+
const messages = await discord.thread(threadId).getMessages()
|
|
55
|
+
const match = messages.find((message) => {
|
|
56
|
+
return (
|
|
57
|
+
message.author.id === discord.botUserId
|
|
58
|
+
&& message.content.includes(text)
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
if (match) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Unexpected bot message containing ${JSON.stringify(text)} while it should still be queued`,
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
await new Promise<void>((resolve) => {
|
|
67
|
+
setTimeout(resolve, 20)
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('queue drain after question select answer', () => {
|
|
73
|
+
const ctx = setupQueueAdvancedSuite({
|
|
74
|
+
channelId: TEXT_CHANNEL_ID,
|
|
75
|
+
channelName: 'qa-question-select-drain',
|
|
76
|
+
dirName: 'qa-question-select-drain',
|
|
77
|
+
username: 'question-select-tester',
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test(
|
|
81
|
+
'queued message drains after answering question via dropdown select',
|
|
82
|
+
async () => {
|
|
83
|
+
// 1. Send a message that triggers the question tool
|
|
84
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
85
|
+
content: 'QUESTION_SELECT_QUEUE_MARKER',
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
89
|
+
timeout: 8_000,
|
|
90
|
+
predicate: (t) => {
|
|
91
|
+
return t.name === 'QUESTION_SELECT_QUEUE_MARKER'
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const th = ctx.discord.thread(thread.id)
|
|
96
|
+
|
|
97
|
+
// 2. Wait for the question dropdown message to appear in Discord.
|
|
98
|
+
// Uses visible message wait instead of internal Map polling which
|
|
99
|
+
// is too timing-sensitive on CI.
|
|
100
|
+
const questionMessages = await waitForBotMessageContaining({
|
|
101
|
+
discord: ctx.discord,
|
|
102
|
+
threadId: thread.id,
|
|
103
|
+
text: 'How to proceed?',
|
|
104
|
+
timeout: 12_000,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Get the pending question context hash from the internal map.
|
|
108
|
+
// By this point the question message is visible so the context must exist.
|
|
109
|
+
const pending = await waitForPendingQuestion({
|
|
110
|
+
threadId: thread.id,
|
|
111
|
+
timeoutMs: 8_000,
|
|
112
|
+
})
|
|
113
|
+
const questionMsg = questionMessages.find((m) => {
|
|
114
|
+
return m.content.includes('How to proceed?')
|
|
115
|
+
})!
|
|
116
|
+
expect(questionMsg).toBeTruthy()
|
|
117
|
+
|
|
118
|
+
// 3. Queue a message while question is pending
|
|
119
|
+
const { id: queueInteractionId } = await th.user(TEST_USER_ID)
|
|
120
|
+
.runSlashCommand({
|
|
121
|
+
name: 'queue',
|
|
122
|
+
options: [{ name: 'message', type: 3, value: 'Reply with exactly: post-question-drain' }],
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const queueAck = await th.waitForInteractionAck({
|
|
126
|
+
interactionId: queueInteractionId,
|
|
127
|
+
timeout: 8_000,
|
|
128
|
+
})
|
|
129
|
+
if (!queueAck.messageId) {
|
|
130
|
+
throw new Error('Expected /queue response message id')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 4. The first queued item should be handed off immediately even while
|
|
134
|
+
// the question is still pending, so the visible dispatch indicator
|
|
135
|
+
// appears before the user answers the dropdown.
|
|
136
|
+
await waitForBotMessageContaining({
|
|
137
|
+
discord: ctx.discord,
|
|
138
|
+
threadId: thread.id,
|
|
139
|
+
text: '» **question-select-tester:** Reply with exactly: post-question-drain',
|
|
140
|
+
timeout: 8_000,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// 5. Answer the question via dropdown select (pick first option "Alpha")
|
|
144
|
+
const interaction = await th.user(TEST_USER_ID).selectMenu({
|
|
145
|
+
messageId: questionMsg.id,
|
|
146
|
+
customId: `ask_question:${pending.contextHash}:0`,
|
|
147
|
+
values: ['0'],
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
await th.waitForInteractionAck({
|
|
151
|
+
interactionId: interaction.id,
|
|
152
|
+
timeout: 8_000,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// 6. Wait for footer from the drained queued message
|
|
156
|
+
await waitForFooterMessage({
|
|
157
|
+
discord: ctx.discord,
|
|
158
|
+
threadId: thread.id,
|
|
159
|
+
timeout: 8_000,
|
|
160
|
+
afterMessageIncludes: '» **question-select-tester:**',
|
|
161
|
+
afterAuthorId: ctx.discord.botUserId,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const timeline = await th.text({ showInteractions: true })
|
|
165
|
+
expect(timeline).toMatchInlineSnapshot(`
|
|
166
|
+
"--- from: user (question-select-tester)
|
|
167
|
+
QUESTION_SELECT_QUEUE_MARKER
|
|
168
|
+
--- from: assistant (TestBot)
|
|
169
|
+
**Select action**
|
|
170
|
+
How to proceed?
|
|
171
|
+
✓ _Alpha_
|
|
172
|
+
[user interaction]
|
|
173
|
+
» **question-select-tester:** Reply with exactly: post-question-drain
|
|
174
|
+
Queued message (position 1)
|
|
175
|
+
[user selects dropdown: 0]
|
|
176
|
+
⬥ ok
|
|
177
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
178
|
+
`)
|
|
179
|
+
expect(timeline).toContain('QUESTION_SELECT_QUEUE_MARKER')
|
|
180
|
+
expect(timeline).toContain('How to proceed?')
|
|
181
|
+
expect(timeline).toContain('[user selects dropdown: 0]')
|
|
182
|
+
expect(timeline).toContain('» **question-select-tester:** Reply with exactly: post-question-drain')
|
|
183
|
+
expect(timeline).toContain('⬥ ok')
|
|
184
|
+
expect(timeline).toContain('*project ⋅ main ⋅')
|
|
185
|
+
},
|
|
186
|
+
20_000,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
test(
|
|
190
|
+
'only the first queued message is handed off after dropdown answer',
|
|
191
|
+
async () => {
|
|
192
|
+
const marker = 'QUESTION_SELECT_QUEUE_MARKER second-test'
|
|
193
|
+
|
|
194
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
195
|
+
content: marker,
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
199
|
+
timeout: 8_000,
|
|
200
|
+
predicate: (t) => {
|
|
201
|
+
return t.name === marker
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const th = ctx.discord.thread(thread.id)
|
|
206
|
+
|
|
207
|
+
const questionMessages = await waitForBotMessageContaining({
|
|
208
|
+
discord: ctx.discord,
|
|
209
|
+
threadId: thread.id,
|
|
210
|
+
text: 'How to proceed?',
|
|
211
|
+
timeout: 12_000,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const pending = await waitForPendingQuestion({
|
|
215
|
+
threadId: thread.id,
|
|
216
|
+
timeoutMs: 8_000,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const questionMsg = questionMessages.find((message) => {
|
|
220
|
+
return message.content.includes('How to proceed?')
|
|
221
|
+
})
|
|
222
|
+
expect(questionMsg).toBeTruthy()
|
|
223
|
+
if (!questionMsg) {
|
|
224
|
+
throw new Error('Expected question message')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const firstQueuedPrompt = 'SLOW_ABORT_MARKER run long response'
|
|
228
|
+
const secondQueuedPrompt = 'Reply with exactly: post-question-second'
|
|
229
|
+
|
|
230
|
+
const { id: firstQueueInteractionId } = await th.user(TEST_USER_ID)
|
|
231
|
+
.runSlashCommand({
|
|
232
|
+
name: 'queue',
|
|
233
|
+
options: [{ name: 'message', type: 3, value: firstQueuedPrompt }],
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
await th.waitForInteractionAck({
|
|
237
|
+
interactionId: firstQueueInteractionId,
|
|
238
|
+
timeout: 8_000,
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const { id: secondQueueInteractionId } = await th.user(TEST_USER_ID)
|
|
242
|
+
.runSlashCommand({
|
|
243
|
+
name: 'queue',
|
|
244
|
+
options: [{ name: 'message', type: 3, value: secondQueuedPrompt }],
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
await th.waitForInteractionAck({
|
|
248
|
+
interactionId: secondQueueInteractionId,
|
|
249
|
+
timeout: 8_000,
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
const interaction = await th.user(TEST_USER_ID).selectMenu({
|
|
253
|
+
messageId: questionMsg.id,
|
|
254
|
+
customId: `ask_question:${pending.contextHash}:0`,
|
|
255
|
+
values: ['0'],
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
await th.waitForInteractionAck({
|
|
259
|
+
interactionId: interaction.id,
|
|
260
|
+
timeout: 8_000,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
await waitForBotMessageContaining({
|
|
264
|
+
discord: ctx.discord,
|
|
265
|
+
threadId: thread.id,
|
|
266
|
+
text: `» **question-select-tester:** ${firstQueuedPrompt}`,
|
|
267
|
+
timeout: 8_000,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
await expectNoBotMessageContaining({
|
|
271
|
+
discord: ctx.discord,
|
|
272
|
+
threadId: thread.id,
|
|
273
|
+
text: `» **question-select-tester:** ${secondQueuedPrompt}`,
|
|
274
|
+
timeout: 200,
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
await waitForFooterMessage({
|
|
278
|
+
discord: ctx.discord,
|
|
279
|
+
threadId: thread.id,
|
|
280
|
+
timeout: 8_000,
|
|
281
|
+
afterMessageIncludes: `» **question-select-tester:** ${firstQueuedPrompt}`,
|
|
282
|
+
afterAuthorId: ctx.discord.botUserId,
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
await waitForBotMessageContaining({
|
|
286
|
+
discord: ctx.discord,
|
|
287
|
+
threadId: thread.id,
|
|
288
|
+
text: `» **question-select-tester:** ${secondQueuedPrompt}`,
|
|
289
|
+
timeout: 8_000,
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
await waitForFooterMessage({
|
|
293
|
+
discord: ctx.discord,
|
|
294
|
+
threadId: thread.id,
|
|
295
|
+
timeout: 8_000,
|
|
296
|
+
afterMessageIncludes: `» **question-select-tester:** ${secondQueuedPrompt}`,
|
|
297
|
+
afterAuthorId: ctx.discord.botUserId,
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
const timeline = await th.text({ showInteractions: true })
|
|
301
|
+
expect(timeline).toMatchInlineSnapshot(`
|
|
302
|
+
"--- from: user (question-select-tester)
|
|
303
|
+
QUESTION_SELECT_QUEUE_MARKER second-test
|
|
304
|
+
--- from: assistant (TestBot)
|
|
305
|
+
**Select action**
|
|
306
|
+
How to proceed?
|
|
307
|
+
✓ _Alpha_
|
|
308
|
+
[user interaction]
|
|
309
|
+
» **question-select-tester:** SLOW_ABORT_MARKER run long response
|
|
310
|
+
Queued message (position 1)
|
|
311
|
+
[user interaction]
|
|
312
|
+
Queued message (position 1)
|
|
313
|
+
[user selects dropdown: 0]
|
|
314
|
+
⬥ slow-response-started
|
|
315
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
316
|
+
» **question-select-tester:** Reply with exactly: post-question-second
|
|
317
|
+
⬥ ok
|
|
318
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
319
|
+
`)
|
|
320
|
+
expect(timeline).toContain(`» **question-select-tester:** ${firstQueuedPrompt}`)
|
|
321
|
+
expect(timeline).toContain('⬥ slow-response-started')
|
|
322
|
+
expect(timeline).toContain(`» **question-select-tester:** ${secondQueuedPrompt}`)
|
|
323
|
+
expect(timeline).toContain('⬥ ok')
|
|
324
|
+
},
|
|
325
|
+
20_000,
|
|
326
|
+
)
|
|
327
|
+
})
|