@otto-assistant/bridge 0.4.92
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-auth-plugin.js +728 -0
- package/dist/anthropic-auth-plugin.test.js +125 -0
- package/dist/anthropic-auth-state.js +231 -0
- package/dist/bin.js +90 -0
- package/dist/channel-management.js +227 -0
- package/dist/cli-parsing.test.js +137 -0
- package/dist/cli-send-thread.e2e.test.js +356 -0
- package/dist/cli.js +3276 -0
- package/dist/commands/abort.js +65 -0
- package/dist/commands/action-buttons.js +245 -0
- package/dist/commands/add-project.js +113 -0
- package/dist/commands/agent.js +335 -0
- package/dist/commands/ask-question.js +274 -0
- package/dist/commands/btw.js +116 -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/file-upload.js +275 -0
- package/dist/commands/fork.js +220 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +885 -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 +159 -0
- package/dist/commands/model-variant.js +364 -0
- package/dist/commands/model.js +776 -0
- package/dist/commands/new-worktree.js +366 -0
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/permissions.js +274 -0
- package/dist/commands/queue.js +206 -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/types.js +2 -0
- package/dist/commands/undo-redo.js +305 -0
- package/dist/commands/unset-model.js +138 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +155 -0
- package/dist/commands/verbosity.js +125 -0
- package/dist/commands/worktree-settings.js +43 -0
- package/dist/commands/worktrees.js +410 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +94 -0
- package/dist/context-awareness-plugin.js +363 -0
- package/dist/context-awareness-plugin.test.js +124 -0
- package/dist/critique-utils.js +95 -0
- package/dist/database.js +1310 -0
- package/dist/db.js +251 -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 +1008 -0
- package/dist/discord-command-registration.js +524 -0
- package/dist/discord-urls.js +81 -0
- package/dist/discord-utils.js +591 -0
- package/dist/discord-utils.test.js +134 -0
- package/dist/errors.js +157 -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 +302 -0
- package/dist/format-tables.test.js +308 -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 +483 -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 +52 -0
- package/dist/generated/internal/class.js +49 -0
- package/dist/generated/internal/prismaNamespace.js +253 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +223 -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 +263 -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 +397 -0
- package/dist/ipc-polling.js +252 -0
- package/dist/ipc-tools-plugin.js +193 -0
- package/dist/kimaki-digital-twin.e2e.test.js +161 -0
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
- package/dist/kimaki-opencode-plugin.js +17 -0
- package/dist/kimaki-opencode-plugin.test.js +98 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +165 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +257 -0
- package/dist/message-finish-field.e2e.test.js +165 -0
- package/dist/message-formatting.js +413 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/message-preprocessing.js +330 -0
- package/dist/onboarding-tutorial.js +172 -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 +129 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +361 -0
- package/dist/opencode-interrupt-plugin.test.js +458 -0
- package/dist/opencode.js +861 -0
- package/dist/otto/branding.js +22 -0
- package/dist/otto/index.js +21 -0
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/patch-text-parser.js +97 -0
- package/dist/plugin-logger.js +59 -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 +786 -0
- package/dist/queue-advanced-footer.e2e.test.js +472 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -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 +120 -0
- package/dist/runtime-idle-sweeper.js +52 -0
- package/dist/runtime-lifecycle.e2e.test.js +508 -0
- package/dist/sentry.js +23 -0
- package/dist/session-handler/agent-utils.js +67 -0
- package/dist/session-handler/event-stream-state.js +420 -0
- package/dist/session-handler/event-stream-state.test.js +563 -0
- package/dist/session-handler/model-utils.js +124 -0
- package/dist/session-handler/opencode-session-event-log.js +94 -0
- package/dist/session-handler/thread-runtime-state.js +104 -0
- package/dist/session-handler/thread-session-runtime.js +3258 -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 +80 -0
- package/dist/startup-service.js +153 -0
- package/dist/startup-time.e2e.test.js +296 -0
- package/dist/store.js +17 -0
- package/dist/system-message.js +613 -0
- package/dist/system-message.test.js +602 -0
- package/dist/task-runner.js +295 -0
- package/dist/task-schedule.js +209 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/test-utils.js +299 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +999 -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 +114 -0
- package/dist/utils.js +144 -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 +447 -0
- package/dist/voice.test.js +235 -0
- package/dist/wait-session.js +94 -0
- package/dist/websockify.js +69 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-lifecycle.e2e.test.js +308 -0
- package/dist/worktree-utils.js +3 -0
- package/dist/worktrees.js +929 -0
- package/dist/worktrees.test.js +189 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +98 -0
- package/schema.prisma +295 -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/gitchamber/SKILL.md +93 -0
- package/skills/goke/SKILL.md +644 -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/new-skill/SKILL.md +211 -0
- package/skills/npm-package/SKILL.md +239 -0
- package/skills/playwriter/SKILL.md +35 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/spiceflow/SKILL.md +14 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/usecomputer/SKILL.md +264 -0
- package/skills/x-articles/SKILL.md +554 -0
- package/skills/zele/SKILL.md +112 -0
- package/skills/zustand-centralized-state/SKILL.md +1004 -0
- package/src/agent-model.e2e.test.ts +976 -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-auth-plugin.test.ts +159 -0
- package/src/anthropic-auth-plugin.ts +861 -0
- package/src/anthropic-auth-state.ts +282 -0
- package/src/bin.ts +111 -0
- package/src/channel-management.ts +334 -0
- package/src/cli-parsing.test.ts +195 -0
- package/src/cli-send-thread.e2e.test.ts +464 -0
- package/src/cli.ts +4581 -0
- package/src/commands/abort.ts +89 -0
- package/src/commands/action-buttons.ts +364 -0
- package/src/commands/add-project.ts +149 -0
- package/src/commands/agent.ts +473 -0
- package/src/commands/ask-question.ts +390 -0
- package/src/commands/btw.ts +164 -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/file-upload.ts +389 -0
- package/src/commands/fork.ts +321 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +1173 -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 +223 -0
- package/src/commands/model-variant.ts +483 -0
- package/src/commands/model.ts +1053 -0
- package/src/commands/new-worktree.ts +510 -0
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/permissions.ts +397 -0
- package/src/commands/queue.ts +271 -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/types.ts +25 -0
- package/src/commands/undo-redo.ts +386 -0
- package/src/commands/unset-model.ts +173 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +198 -0
- package/src/commands/verbosity.ts +173 -0
- package/src/commands/worktree-settings.ts +70 -0
- package/src/commands/worktrees.ts +552 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +111 -0
- package/src/context-awareness-plugin.test.ts +142 -0
- package/src/context-awareness-plugin.ts +510 -0
- package/src/critique-utils.ts +139 -0
- package/src/database.ts +1876 -0
- package/src/db.test.ts +162 -0
- package/src/db.ts +286 -0
- package/src/debounce-timeout.ts +43 -0
- package/src/debounced-process-flush.ts +104 -0
- package/src/discord-bot.ts +1330 -0
- package/src/discord-command-registration.ts +693 -0
- package/src/discord-urls.ts +88 -0
- package/src/discord-utils.test.ts +153 -0
- package/src/discord-utils.ts +800 -0
- package/src/errors.ts +201 -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 +335 -0
- package/src/format-tables.ts +445 -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 +640 -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 +736 -0
- package/src/generated/enums.ts +88 -0
- package/src/generated/internal/class.ts +384 -0
- package/src/generated/internal/prismaNamespace.ts +2386 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1656 -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 +314 -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 +576 -0
- package/src/ipc-polling.ts +326 -0
- package/src/ipc-tools-plugin.ts +236 -0
- package/src/kimaki-digital-twin.e2e.test.ts +199 -0
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
- package/src/kimaki-opencode-plugin.test.ts +108 -0
- package/src/kimaki-opencode-plugin.ts +18 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +208 -0
- package/src/markdown.test.ts +308 -0
- package/src/markdown.ts +410 -0
- package/src/message-finish-field.e2e.test.ts +192 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +533 -0
- package/src/message-preprocessing.ts +455 -0
- package/src/onboarding-tutorial.ts +176 -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 +188 -0
- package/src/opencode-interrupt-plugin.test.ts +677 -0
- package/src/opencode-interrupt-plugin.ts +477 -0
- package/src/opencode.ts +1110 -0
- package/src/otto/branding.ts +23 -0
- package/src/otto/index.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 +68 -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 +873 -0
- package/src/queue-advanced-footer.e2e.test.ts +576 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -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 +152 -0
- package/src/runtime-idle-sweeper.ts +76 -0
- package/src/runtime-lifecycle.e2e.test.ts +641 -0
- package/src/schema.sql +173 -0
- package/src/sentry.ts +26 -0
- package/src/session-handler/agent-utils.ts +97 -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 +645 -0
- package/src/session-handler/event-stream-state.ts +608 -0
- package/src/session-handler/model-utils.ts +183 -0
- package/src/session-handler/opencode-session-event-log.ts +130 -0
- package/src/session-handler/thread-runtime-state.ts +212 -0
- package/src/session-handler/thread-session-runtime.ts +4281 -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 +112 -0
- package/src/startup-service.ts +200 -0
- package/src/startup-time.e2e.test.ts +373 -0
- package/src/store.ts +122 -0
- package/src/system-message.test.ts +612 -0
- package/src/system-message.ts +723 -0
- package/src/task-runner.ts +421 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +311 -0
- package/src/test-utils.ts +435 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +1219 -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 +127 -0
- package/src/utils.ts +212 -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 +627 -0
- package/src/wait-session.ts +147 -0
- package/src/websockify.ts +101 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-lifecycle.e2e.test.ts +391 -0
- package/src/worktree-utils.ts +4 -0
- package/src/worktrees.test.ts +223 -0
- package/src/worktrees.ts +1294 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
// Gateway-proxy integration test.
|
|
2
|
+
// Starts a discord-digital-twin (fake Discord), a gateway-proxy Rust binary
|
|
3
|
+
// in front of it, and the kimaki bot connecting through the proxy.
|
|
4
|
+
// Validates that messages create threads, bot replies, and multi-tenant
|
|
5
|
+
// guild filtering routes events to the right clients.
|
|
6
|
+
//
|
|
7
|
+
// Requires the gateway-proxy binary at gateway-proxy/target/release/gateway-proxy.
|
|
8
|
+
// If not found, all tests are skipped.
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import url from 'node:url'
|
|
13
|
+
import net from 'node:net'
|
|
14
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
15
|
+
import { describe, test, expect, beforeAll, afterAll } from 'vitest'
|
|
16
|
+
import {
|
|
17
|
+
ChannelType,
|
|
18
|
+
Client,
|
|
19
|
+
GatewayIntentBits,
|
|
20
|
+
Partials,
|
|
21
|
+
Routes,
|
|
22
|
+
} from 'discord.js'
|
|
23
|
+
import { DigitalDiscord } from 'discord-digital-twin/src'
|
|
24
|
+
import {
|
|
25
|
+
buildDeterministicOpencodeConfig,
|
|
26
|
+
type DeterministicMatcher,
|
|
27
|
+
} from 'opencode-deterministic-provider'
|
|
28
|
+
import { startHranaServer, stopHranaServer } from './hrana-server.js'
|
|
29
|
+
import {
|
|
30
|
+
setBotToken,
|
|
31
|
+
initDatabase,
|
|
32
|
+
closeDatabase,
|
|
33
|
+
setChannelDirectory,
|
|
34
|
+
} from './database.js'
|
|
35
|
+
import { setDataDir } from './config.js'
|
|
36
|
+
import type { VerbosityLevel } from './database.js'
|
|
37
|
+
import { startDiscordBot } from './discord-bot.js'
|
|
38
|
+
import {
|
|
39
|
+
chooseLockPort,
|
|
40
|
+
cleanupTestSessions,
|
|
41
|
+
initTestGitRepo,
|
|
42
|
+
waitForFooterMessage,
|
|
43
|
+
} from './test-utils.js'
|
|
44
|
+
import { stopOpencodeServer } from './opencode.js'
|
|
45
|
+
import { createDiscordRest } from './discord-urls.js'
|
|
46
|
+
import { store } from './store.js'
|
|
47
|
+
|
|
48
|
+
// --- Constants ---
|
|
49
|
+
|
|
50
|
+
const BINARY_PATH = path.resolve(
|
|
51
|
+
process.cwd(),
|
|
52
|
+
'..',
|
|
53
|
+
'gateway-proxy',
|
|
54
|
+
'target',
|
|
55
|
+
'release',
|
|
56
|
+
'gateway-proxy',
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const TEST_USER_ID = '900000000000000001'
|
|
60
|
+
const CHANNEL_1_ID = '900000000000000010'
|
|
61
|
+
const CHANNEL_2_ID = '900000000000000020'
|
|
62
|
+
const GUILD_1_ID = '900000000000000100'
|
|
63
|
+
const GUILD_2_ID = '900000000000000200'
|
|
64
|
+
|
|
65
|
+
const binaryExists = fs.existsSync(BINARY_PATH)
|
|
66
|
+
|
|
67
|
+
// --- Helpers ---
|
|
68
|
+
|
|
69
|
+
function getAvailablePort(): Promise<number> {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const srv = net.createServer()
|
|
72
|
+
srv.listen(0, () => {
|
|
73
|
+
const addr = srv.address()
|
|
74
|
+
if (!addr || typeof addr === 'string') {
|
|
75
|
+
srv.close()
|
|
76
|
+
reject(new Error('Failed to get port'))
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
const port = addr.port
|
|
80
|
+
srv.close(() => {
|
|
81
|
+
resolve(port)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createRunDirectories() {
|
|
88
|
+
const root = path.resolve(process.cwd(), 'tmp', 'gateway-proxy-e2e')
|
|
89
|
+
fs.mkdirSync(root, { recursive: true })
|
|
90
|
+
const dataDir = fs.mkdtempSync(path.join(root, 'data-'))
|
|
91
|
+
const projectDirectory = path.join(root, 'project')
|
|
92
|
+
fs.mkdirSync(projectDirectory, { recursive: true })
|
|
93
|
+
initTestGitRepo(projectDirectory)
|
|
94
|
+
return { root, dataDir, projectDirectory }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createDiscordJsClient({ restUrl }: { restUrl: string }) {
|
|
98
|
+
return new Client({
|
|
99
|
+
intents: [
|
|
100
|
+
GatewayIntentBits.Guilds,
|
|
101
|
+
GatewayIntentBits.GuildMessages,
|
|
102
|
+
GatewayIntentBits.MessageContent,
|
|
103
|
+
],
|
|
104
|
+
partials: [Partials.Channel, Partials.Message, Partials.User, Partials.ThreadMember],
|
|
105
|
+
rest: { api: restUrl, version: '10' },
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hasStringId(value: unknown): value is { id: string } {
|
|
110
|
+
if (typeof value !== 'object' || value === null) {
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
if (!('id' in value)) {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
return typeof value.id === 'string'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createMatchers(): DeterministicMatcher[] {
|
|
120
|
+
const defaultReply: DeterministicMatcher = {
|
|
121
|
+
id: 'default-reply',
|
|
122
|
+
priority: 10,
|
|
123
|
+
when: { lastMessageRole: 'user' },
|
|
124
|
+
then: {
|
|
125
|
+
parts: [
|
|
126
|
+
{ type: 'stream-start', warnings: [] },
|
|
127
|
+
{ type: 'text-start', id: 'reply' },
|
|
128
|
+
{ type: 'text-delta', id: 'reply', delta: 'gateway-proxy-reply' },
|
|
129
|
+
{ type: 'text-end', id: 'reply' },
|
|
130
|
+
{
|
|
131
|
+
type: 'finish',
|
|
132
|
+
finishReason: 'stop',
|
|
133
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
return [defaultReply]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function waitForProxyReady({
|
|
142
|
+
port,
|
|
143
|
+
timeoutMs = 30_000,
|
|
144
|
+
}: {
|
|
145
|
+
port: number
|
|
146
|
+
timeoutMs?: number
|
|
147
|
+
}): Promise<void> {
|
|
148
|
+
const start = Date.now()
|
|
149
|
+
while (Date.now() - start < timeoutMs) {
|
|
150
|
+
try {
|
|
151
|
+
const res = await fetch(`http://127.0.0.1:${port}/shard-count`)
|
|
152
|
+
if (res.ok) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
// not ready yet
|
|
157
|
+
}
|
|
158
|
+
await new Promise((r) => {
|
|
159
|
+
setTimeout(r, 500)
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
throw new Error(`gateway-proxy not ready after ${timeoutMs}ms`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function startGatewayProxy({
|
|
166
|
+
configDir,
|
|
167
|
+
port,
|
|
168
|
+
twinPort,
|
|
169
|
+
botToken,
|
|
170
|
+
gatewayUrl,
|
|
171
|
+
}: {
|
|
172
|
+
configDir: string
|
|
173
|
+
port: number
|
|
174
|
+
twinPort: number
|
|
175
|
+
botToken: string
|
|
176
|
+
gatewayUrl: string
|
|
177
|
+
}): { process: ChildProcess; configPath: string } {
|
|
178
|
+
const config = {
|
|
179
|
+
log_level: 'info',
|
|
180
|
+
token: botToken,
|
|
181
|
+
intents: 32511,
|
|
182
|
+
shards: 1,
|
|
183
|
+
port,
|
|
184
|
+
validate_token: true,
|
|
185
|
+
gateway_url: gatewayUrl,
|
|
186
|
+
twilight_http_proxy: `127.0.0.1:${twinPort}`,
|
|
187
|
+
externally_accessible_url: `ws://127.0.0.1:${port}`,
|
|
188
|
+
cache: {
|
|
189
|
+
channels: true,
|
|
190
|
+
presences: false,
|
|
191
|
+
emojis: false,
|
|
192
|
+
current_member: true,
|
|
193
|
+
members: false,
|
|
194
|
+
roles: true,
|
|
195
|
+
scheduled_events: false,
|
|
196
|
+
stage_instances: false,
|
|
197
|
+
stickers: false,
|
|
198
|
+
users: false,
|
|
199
|
+
voice_states: false,
|
|
200
|
+
},
|
|
201
|
+
clients: {
|
|
202
|
+
'client-a': {
|
|
203
|
+
secret: 'secret-a',
|
|
204
|
+
guilds: [GUILD_1_ID],
|
|
205
|
+
},
|
|
206
|
+
'client-b': {
|
|
207
|
+
secret: 'secret-b',
|
|
208
|
+
guilds: [GUILD_2_ID],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const configPath = path.join(configDir, 'config.json')
|
|
214
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
|
|
215
|
+
|
|
216
|
+
const child = spawn(BINARY_PATH, [], {
|
|
217
|
+
cwd: configDir,
|
|
218
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
219
|
+
env: { ...process.env, RUST_LOG: 'debug' },
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const showLogs = !!process.env['KIMAKI_TEST_LOGS']
|
|
223
|
+
child.stdout?.on('data', (data: Buffer) => {
|
|
224
|
+
const line = data.toString().trim()
|
|
225
|
+
if (line && showLogs) {
|
|
226
|
+
console.log(`[gateway-proxy] ${line}`)
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
child.stderr?.on('data', (data: Buffer) => {
|
|
230
|
+
const line = data.toString().trim()
|
|
231
|
+
if (line && showLogs) {
|
|
232
|
+
console.log(`[gateway-proxy] ${line}`)
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
return { process: child, configPath }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// --- Test suite ---
|
|
240
|
+
|
|
241
|
+
const describeIf = binaryExists ? describe : describe.skip
|
|
242
|
+
|
|
243
|
+
describeIf('gateway-proxy e2e', () => {
|
|
244
|
+
let discord: DigitalDiscord
|
|
245
|
+
let proxyProcess: ChildProcess
|
|
246
|
+
let botClient: Client
|
|
247
|
+
let directories: ReturnType<typeof createRunDirectories>
|
|
248
|
+
let proxyPort: number
|
|
249
|
+
let previousDefaultVerbosity: VerbosityLevel | undefined
|
|
250
|
+
let firstThreadId: string
|
|
251
|
+
let testStartTime = Date.now()
|
|
252
|
+
|
|
253
|
+
beforeAll(async () => {
|
|
254
|
+
testStartTime = Date.now()
|
|
255
|
+
const lockPort = chooseLockPort({ key: CHANNEL_1_ID })
|
|
256
|
+
directories = createRunDirectories()
|
|
257
|
+
process.env['KIMAKI_LOCK_PORT'] = String(lockPort)
|
|
258
|
+
process.env['KIMAKI_VITEST'] = '1'
|
|
259
|
+
setDataDir(directories.dataDir)
|
|
260
|
+
previousDefaultVerbosity = store.getState().defaultVerbosity
|
|
261
|
+
store.setState({ defaultVerbosity: 'text_only' })
|
|
262
|
+
|
|
263
|
+
const digitalDiscordDbPath = path.join(
|
|
264
|
+
directories.dataDir,
|
|
265
|
+
'digital-discord.db',
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
proxyPort = await getAvailablePort()
|
|
269
|
+
|
|
270
|
+
// Start digital-twin with 2 guilds, each with a text channel.
|
|
271
|
+
// gatewayUrlOverride makes GET /gateway/bot return the proxy's URL
|
|
272
|
+
// so discord.js clients connect through the proxy, not directly to twin.
|
|
273
|
+
discord = new DigitalDiscord({
|
|
274
|
+
guilds: [
|
|
275
|
+
{
|
|
276
|
+
id: GUILD_1_ID,
|
|
277
|
+
name: 'Guild One',
|
|
278
|
+
ownerId: TEST_USER_ID,
|
|
279
|
+
channels: [
|
|
280
|
+
{ id: CHANNEL_1_ID, name: 'general-1', type: ChannelType.GuildText },
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: GUILD_2_ID,
|
|
285
|
+
name: 'Guild Two',
|
|
286
|
+
ownerId: TEST_USER_ID,
|
|
287
|
+
channels: [
|
|
288
|
+
{ id: CHANNEL_2_ID, name: 'general-2', type: ChannelType.GuildText },
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
users: [{ id: TEST_USER_ID, username: 'proxy-tester' }],
|
|
293
|
+
gatewayUrlOverride: `ws://127.0.0.1:${proxyPort}`,
|
|
294
|
+
dbUrl: `file:${digitalDiscordDbPath}`,
|
|
295
|
+
})
|
|
296
|
+
await discord.start()
|
|
297
|
+
|
|
298
|
+
// Write opencode.json with deterministic provider
|
|
299
|
+
const providerNpm = url
|
|
300
|
+
.pathToFileURL(
|
|
301
|
+
path.resolve(process.cwd(), '..', 'opencode-deterministic-provider', 'src', 'index.ts'),
|
|
302
|
+
)
|
|
303
|
+
.toString()
|
|
304
|
+
|
|
305
|
+
const opencodeConfig = buildDeterministicOpencodeConfig({
|
|
306
|
+
providerName: 'deterministic-provider',
|
|
307
|
+
providerNpm,
|
|
308
|
+
model: 'deterministic-v2',
|
|
309
|
+
smallModel: 'deterministic-v2',
|
|
310
|
+
settings: { strict: false, matchers: createMatchers() },
|
|
311
|
+
})
|
|
312
|
+
fs.writeFileSync(
|
|
313
|
+
path.join(directories.projectDirectory, 'opencode.json'),
|
|
314
|
+
JSON.stringify(opencodeConfig, null, 2),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
// Start gateway-proxy binary pointing at twin
|
|
318
|
+
const proxyConfigDir = path.join(directories.dataDir, 'proxy')
|
|
319
|
+
fs.mkdirSync(proxyConfigDir, { recursive: true })
|
|
320
|
+
|
|
321
|
+
const proxy = startGatewayProxy({
|
|
322
|
+
configDir: proxyConfigDir,
|
|
323
|
+
port: proxyPort,
|
|
324
|
+
twinPort: discord.port,
|
|
325
|
+
botToken: discord.botToken,
|
|
326
|
+
gatewayUrl: discord.gatewayUrl,
|
|
327
|
+
})
|
|
328
|
+
proxyProcess = proxy.process
|
|
329
|
+
|
|
330
|
+
// Wait for proxy to be ready (HTTP server up)
|
|
331
|
+
await waitForProxyReady({ port: proxyPort, timeoutMs: 30_000 })
|
|
332
|
+
|
|
333
|
+
// Initialize kimaki database
|
|
334
|
+
const dbPath = path.join(directories.dataDir, 'discord-sessions.db')
|
|
335
|
+
const hranaResult = await startHranaServer({ dbPath })
|
|
336
|
+
if (hranaResult instanceof Error) {
|
|
337
|
+
throw hranaResult
|
|
338
|
+
}
|
|
339
|
+
process.env['KIMAKI_DB_URL'] = hranaResult
|
|
340
|
+
await initDatabase()
|
|
341
|
+
await setBotToken(discord.botUserId, discord.botToken)
|
|
342
|
+
|
|
343
|
+
// Register channel 1 with kimaki (bot will create sessions for messages here)
|
|
344
|
+
await setChannelDirectory({
|
|
345
|
+
channelId: CHANNEL_1_ID,
|
|
346
|
+
directory: directories.projectDirectory,
|
|
347
|
+
channelType: 'text',
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// Start the kimaki bot connected through the proxy
|
|
351
|
+
botClient = createDiscordJsClient({ restUrl: discord.restUrl })
|
|
352
|
+
|
|
353
|
+
await startDiscordBot({
|
|
354
|
+
token: discord.botToken,
|
|
355
|
+
appId: discord.botUserId,
|
|
356
|
+
discordClient: botClient,
|
|
357
|
+
})
|
|
358
|
+
}, 120_000)
|
|
359
|
+
|
|
360
|
+
afterAll(async () => {
|
|
361
|
+
if (directories) {
|
|
362
|
+
await cleanupTestSessions({
|
|
363
|
+
projectDirectory: directories.projectDirectory,
|
|
364
|
+
testStartTime,
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (botClient) {
|
|
369
|
+
botClient.destroy()
|
|
370
|
+
}
|
|
371
|
+
if (proxyProcess && !proxyProcess.killed) {
|
|
372
|
+
proxyProcess.kill('SIGTERM')
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
await stopOpencodeServer()
|
|
376
|
+
await Promise.all([
|
|
377
|
+
closeDatabase().catch(() => {}),
|
|
378
|
+
stopHranaServer().catch(() => {}),
|
|
379
|
+
discord?.stop().catch(() => {}),
|
|
380
|
+
])
|
|
381
|
+
|
|
382
|
+
delete process.env['KIMAKI_LOCK_PORT']
|
|
383
|
+
delete process.env['KIMAKI_DB_URL']
|
|
384
|
+
delete process.env['KIMAKI_VITEST']
|
|
385
|
+
if (previousDefaultVerbosity) {
|
|
386
|
+
store.setState({ defaultVerbosity: previousDefaultVerbosity })
|
|
387
|
+
}
|
|
388
|
+
if (directories) {
|
|
389
|
+
fs.rmSync(directories.dataDir, { recursive: true, force: true })
|
|
390
|
+
}
|
|
391
|
+
}, 30_000)
|
|
392
|
+
|
|
393
|
+
test(
|
|
394
|
+
'message creates thread and bot replies through proxy',
|
|
395
|
+
async () => {
|
|
396
|
+
await discord.channel(CHANNEL_1_ID).user(TEST_USER_ID).sendMessage({
|
|
397
|
+
content: 'hello from gateway proxy test',
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
const thread = await discord.channel(CHANNEL_1_ID).waitForThread({
|
|
401
|
+
timeout: 15_000,
|
|
402
|
+
predicate: (t) => {
|
|
403
|
+
return t.name?.includes('hello from gateway proxy test') ?? false
|
|
404
|
+
},
|
|
405
|
+
})
|
|
406
|
+
expect(thread).toBeDefined()
|
|
407
|
+
expect(thread.id).toBeTruthy()
|
|
408
|
+
firstThreadId = thread.id
|
|
409
|
+
|
|
410
|
+
const reply = await discord.thread(thread.id).waitForBotReply({ timeout: 15_000 })
|
|
411
|
+
|
|
412
|
+
await waitForFooterMessage({
|
|
413
|
+
discord,
|
|
414
|
+
threadId: thread.id,
|
|
415
|
+
timeout: 15_000,
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
419
|
+
"--- from: user (proxy-tester)
|
|
420
|
+
hello from gateway proxy test
|
|
421
|
+
--- from: assistant (TestBot)
|
|
422
|
+
⬥ gateway-proxy-reply
|
|
423
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
424
|
+
`)
|
|
425
|
+
expect(reply).toBeDefined()
|
|
426
|
+
expect(reply.content.trim().length).toBeGreaterThan(0)
|
|
427
|
+
},
|
|
428
|
+
15_000,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
test(
|
|
432
|
+
'follow-up message in thread gets bot reply',
|
|
433
|
+
async () => {
|
|
434
|
+
const existingMessages = await discord.thread(firstThreadId).getMessages()
|
|
435
|
+
const existingIds = new Set(existingMessages.map((m) => m.id))
|
|
436
|
+
|
|
437
|
+
await discord.thread(firstThreadId).user(TEST_USER_ID).sendMessage({
|
|
438
|
+
content: 'follow up through proxy',
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
const reply = await discord.thread(firstThreadId).waitForMessage({
|
|
442
|
+
predicate: (m) => !existingIds.has(m.id) && m.author.id === discord.botUserId,
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
await waitForFooterMessage({
|
|
446
|
+
discord,
|
|
447
|
+
threadId: firstThreadId,
|
|
448
|
+
timeout: 4_000,
|
|
449
|
+
afterMessageIncludes: 'follow up through proxy',
|
|
450
|
+
afterAuthorId: TEST_USER_ID,
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
expect(await discord.thread(firstThreadId).text()).toMatchInlineSnapshot(`
|
|
454
|
+
"--- from: user (proxy-tester)
|
|
455
|
+
hello from gateway proxy test
|
|
456
|
+
--- from: assistant (TestBot)
|
|
457
|
+
⬥ gateway-proxy-reply
|
|
458
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
459
|
+
--- from: user (proxy-tester)
|
|
460
|
+
follow up through proxy
|
|
461
|
+
--- from: assistant (TestBot)
|
|
462
|
+
⬥ gateway-proxy-reply
|
|
463
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
464
|
+
`)
|
|
465
|
+
expect(reply).toBeDefined()
|
|
466
|
+
expect(reply.content.trim().length).toBeGreaterThan(0)
|
|
467
|
+
},
|
|
468
|
+
15_000,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
// Reconnect test lives in gateway-proxy-reconnect.e2e.test.ts.
|
|
472
|
+
// It was here before but kills the proxy mid-suite, breaking shared
|
|
473
|
+
// state (bot/proxy connection) for all subsequent tests.
|
|
474
|
+
|
|
475
|
+
test(
|
|
476
|
+
'shell command via ! prefix in thread',
|
|
477
|
+
async () => {
|
|
478
|
+
const existingMessages = await discord.thread(firstThreadId).getMessages()
|
|
479
|
+
const existingIds = new Set(existingMessages.map((m) => m.id))
|
|
480
|
+
|
|
481
|
+
await discord.thread(firstThreadId).user(TEST_USER_ID).sendMessage({
|
|
482
|
+
content: '!echo proxy-shell-test',
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
// The bot replies with a loading message then edits it with the result.
|
|
486
|
+
// The predicate waits for the edited version containing "exited with".
|
|
487
|
+
const reply = await discord.thread(firstThreadId).waitForMessage({
|
|
488
|
+
predicate: (m) =>
|
|
489
|
+
!existingIds.has(m.id) &&
|
|
490
|
+
m.author.id === discord.botUserId &&
|
|
491
|
+
m.content.includes('exited with'),
|
|
492
|
+
})
|
|
493
|
+
expect(await discord.thread(firstThreadId).text()).toMatchInlineSnapshot(`
|
|
494
|
+
"--- from: user (proxy-tester)
|
|
495
|
+
hello from gateway proxy test
|
|
496
|
+
--- from: assistant (TestBot)
|
|
497
|
+
⬥ gateway-proxy-reply
|
|
498
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
499
|
+
--- from: user (proxy-tester)
|
|
500
|
+
follow up through proxy
|
|
501
|
+
--- from: assistant (TestBot)
|
|
502
|
+
⬥ gateway-proxy-reply
|
|
503
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
504
|
+
--- from: user (proxy-tester)
|
|
505
|
+
!echo proxy-shell-test
|
|
506
|
+
--- from: assistant (TestBot)
|
|
507
|
+
\`echo proxy-shell-test\` exited with 0
|
|
508
|
+
\`\`\`
|
|
509
|
+
proxy-shell-test
|
|
510
|
+
\`\`\`"
|
|
511
|
+
`)
|
|
512
|
+
expect(reply.content).toContain('proxy-shell-test')
|
|
513
|
+
},
|
|
514
|
+
15_000,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
test(
|
|
518
|
+
'second message creates separate thread',
|
|
519
|
+
async () => {
|
|
520
|
+
await discord.channel(CHANNEL_1_ID).user(TEST_USER_ID).sendMessage({
|
|
521
|
+
content: 'second message through proxy',
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
const thread = await discord.channel(CHANNEL_1_ID).waitForThread({
|
|
525
|
+
predicate: (t) =>
|
|
526
|
+
(t.name?.includes('second message through proxy') ?? false) &&
|
|
527
|
+
t.id !== firstThreadId,
|
|
528
|
+
})
|
|
529
|
+
expect(thread).toBeDefined()
|
|
530
|
+
expect(thread.id).not.toBe(firstThreadId)
|
|
531
|
+
|
|
532
|
+
const reply = await discord.thread(thread.id).waitForBotReply()
|
|
533
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
534
|
+
"--- from: user (proxy-tester)
|
|
535
|
+
second message through proxy
|
|
536
|
+
--- from: assistant (TestBot)
|
|
537
|
+
⬥ gateway-proxy-reply"
|
|
538
|
+
`)
|
|
539
|
+
expect(reply).toBeDefined()
|
|
540
|
+
expect(reply.content.trim().length).toBeGreaterThan(0)
|
|
541
|
+
},
|
|
542
|
+
15_000,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
test(
|
|
546
|
+
'guild-2 message does not create thread (guild isolation)',
|
|
547
|
+
async () => {
|
|
548
|
+
await discord.channel(CHANNEL_2_ID).user(TEST_USER_ID).sendMessage({
|
|
549
|
+
content: 'should not create thread in guild 2',
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
// Brief wait for events to propagate through the local system.
|
|
553
|
+
// The proxy filters guild-2 events away from client-a, so no thread
|
|
554
|
+
// should be created. 100ms is more than enough for local event routing.
|
|
555
|
+
await new Promise((r) => {
|
|
556
|
+
setTimeout(r, 100)
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
const threads = await discord.channel(CHANNEL_2_ID).getThreads()
|
|
560
|
+
expect(threads).toHaveLength(0)
|
|
561
|
+
},
|
|
562
|
+
5_000,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
test(
|
|
566
|
+
'slash command routes INTERACTION_CREATE through proxy',
|
|
567
|
+
async () => {
|
|
568
|
+
const { id: interactionId } = await discord
|
|
569
|
+
.channel(CHANNEL_1_ID)
|
|
570
|
+
.user(TEST_USER_ID)
|
|
571
|
+
.runSlashCommand({
|
|
572
|
+
name: 'run-shell-command',
|
|
573
|
+
options: [{ name: 'command', type: 3, value: 'echo proxy-slash-test' }],
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
const ack = await discord.channel(CHANNEL_1_ID).waitForInteractionAck({
|
|
577
|
+
interactionId,
|
|
578
|
+
})
|
|
579
|
+
expect(ack.acknowledged).toBe(true)
|
|
580
|
+
},
|
|
581
|
+
15_000,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
test(
|
|
585
|
+
'REST client operations work through proxy and enforce guild scope',
|
|
586
|
+
async () => {
|
|
587
|
+
const previousBaseUrl = store.getState().discordBaseUrl
|
|
588
|
+
store.setState({ discordBaseUrl: `http://127.0.0.1:${proxyPort}` })
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
const botRest = createDiscordRest(discord.botToken)
|
|
592
|
+
const clientRest = createDiscordRest('client-a:secret-a')
|
|
593
|
+
|
|
594
|
+
const posted = await botRest.post(Routes.channelMessages(CHANNEL_1_ID), {
|
|
595
|
+
body: { content: 'rest-proxy-test-message' },
|
|
596
|
+
})
|
|
597
|
+
expect(hasStringId(posted)).toBe(true)
|
|
598
|
+
if (!hasStringId(posted)) {
|
|
599
|
+
throw new Error('Expected REST message create response to include id')
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const thread = await botRest.post(
|
|
603
|
+
Routes.threads(CHANNEL_1_ID, posted.id),
|
|
604
|
+
{
|
|
605
|
+
body: { name: 'rest-proxy-thread' },
|
|
606
|
+
},
|
|
607
|
+
)
|
|
608
|
+
expect(hasStringId(thread)).toBe(true)
|
|
609
|
+
|
|
610
|
+
const channel = await botRest.get(Routes.channel(CHANNEL_1_ID))
|
|
611
|
+
expect(hasStringId(channel)).toBe(true)
|
|
612
|
+
|
|
613
|
+
const guildChannels = await clientRest.get(
|
|
614
|
+
Routes.guildChannels(GUILD_1_ID),
|
|
615
|
+
)
|
|
616
|
+
expect(Array.isArray(guildChannels)).toBe(true)
|
|
617
|
+
|
|
618
|
+
const forbiddenGuildResponse = await fetch(
|
|
619
|
+
`http://127.0.0.1:${proxyPort}/api/v10${Routes.guildChannels(GUILD_2_ID)}`,
|
|
620
|
+
{
|
|
621
|
+
method: 'GET',
|
|
622
|
+
headers: {
|
|
623
|
+
Authorization: 'Bot client-a:secret-a',
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
)
|
|
627
|
+
expect(forbiddenGuildResponse.status).toBe(403)
|
|
628
|
+
|
|
629
|
+
const gatewayInfo = await clientRest.get(Routes.gatewayBot())
|
|
630
|
+
expect(typeof gatewayInfo).toBe('object')
|
|
631
|
+
|
|
632
|
+
const me = await clientRest.get(Routes.user('@me'))
|
|
633
|
+
expect(hasStringId(me)).toBe(true)
|
|
634
|
+
} finally {
|
|
635
|
+
store.setState({ discordBaseUrl: previousBaseUrl })
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
15_000,
|
|
639
|
+
)
|
|
640
|
+
})
|