kimaki 0.26.0 → 0.28.0
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/dist/agent-model.e2e.test.js +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -228
package/src/voice-handler.ts
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from './discord-utils.js'
|
|
42
42
|
import { requestAudioApiKey } from './commands/gemini-apikey.js'
|
|
43
43
|
import { transcribeAudio, type TranscriptionResult } from './voice.js'
|
|
44
|
-
import { DiscordOperationError, FetchError } from './errors.js'
|
|
44
|
+
import { DiscordOperationError, FetchError, EmptyTranscriptionError, TranscriptionError } from './errors.js'
|
|
45
45
|
import { store } from './store.js'
|
|
46
46
|
import {
|
|
47
47
|
getVoiceAttachmentMatchReason,
|
|
@@ -486,6 +486,7 @@ type ProcessVoiceAttachmentArgs = {
|
|
|
486
486
|
lastSessionContext?: string
|
|
487
487
|
/** Available agents for voice-based agent selection. Passed to the transcription prompt as enum values. */
|
|
488
488
|
agents?: Array<{ name: string; description?: string }>
|
|
489
|
+
canForkSession?: boolean
|
|
489
490
|
}
|
|
490
491
|
|
|
491
492
|
// Per-thread serialization is handled by ThreadSessionRuntime.enqueueIncoming()
|
|
@@ -499,7 +500,8 @@ export async function processVoiceAttachment({
|
|
|
499
500
|
currentSessionContext,
|
|
500
501
|
lastSessionContext,
|
|
501
502
|
agents,
|
|
502
|
-
|
|
503
|
+
canForkSession = false,
|
|
504
|
+
}: ProcessVoiceAttachmentArgs): Promise<TranscriptionResult | TranscriptionError | EmptyTranscriptionError | null> {
|
|
503
505
|
const audioAttachment = Array.from(message.attachments.values()).find(
|
|
504
506
|
(attachment) => isVoiceAttachment(attachment),
|
|
505
507
|
)
|
|
@@ -544,6 +546,11 @@ export async function processVoiceAttachment({
|
|
|
544
546
|
transcription: deterministicConfig.transcription,
|
|
545
547
|
queueMessage: deterministicConfig.queueMessage,
|
|
546
548
|
agent: deterministicConfig.agent,
|
|
549
|
+
sessionAction: deterministicConfig.sessionAction,
|
|
550
|
+
}
|
|
551
|
+
if (!result.transcription.trim()) {
|
|
552
|
+
await sendThreadMessage(thread, 'No request was transcribed. Record another voice message with the task to send.')
|
|
553
|
+
return new EmptyTranscriptionError()
|
|
547
554
|
}
|
|
548
555
|
voiceLogger.log(
|
|
549
556
|
`[DETERMINISTIC] Returning canned transcription: "${result.transcription}"${result.queueMessage ? ' [QUEUE]' : ''}`,
|
|
@@ -566,6 +573,10 @@ export async function processVoiceAttachment({
|
|
|
566
573
|
thread,
|
|
567
574
|
`📝 **Transcribed message:** ${escapeDiscordFormatting(result.transcription)}`,
|
|
568
575
|
)
|
|
576
|
+
if (result.sessionAction === 'btw' && !canForkSession) {
|
|
577
|
+
await sendThreadMessage(thread, 'A contextual side chat needs an existing session. Nothing was sent to the agent. Retry from a session, or ask to create a fresh chat with this request.')
|
|
578
|
+
return new TranscriptionError({ reason: 'Contextual routing is unavailable without a source session' })
|
|
579
|
+
}
|
|
569
580
|
return result
|
|
570
581
|
}
|
|
571
582
|
|
|
@@ -627,7 +638,7 @@ export async function processVoiceAttachment({
|
|
|
627
638
|
// No user-configured key: gateway-mode installs get one free transcription
|
|
628
639
|
// via the kimaki.dev Whisper fallback before we bother the user with the
|
|
629
640
|
// "add API key" dialog. This fallback has no tool-calling, so it can't
|
|
630
|
-
// detect queueMessage/agent hints spoken in the voice message the way the
|
|
641
|
+
// detect queueMessage/sessionAction/agent hints spoken in the voice message the way the
|
|
631
642
|
// OpenAI/Gemini path can.
|
|
632
643
|
let gatewayTranscription: string | undefined
|
|
633
644
|
if (!transcriptionApiKey) {
|
|
@@ -677,14 +688,18 @@ export async function processVoiceAttachment({
|
|
|
677
688
|
currentSessionContext,
|
|
678
689
|
lastSessionContext,
|
|
679
690
|
agents,
|
|
691
|
+
canForkSession,
|
|
680
692
|
})
|
|
681
693
|
|
|
682
694
|
if (transcription instanceof Error) {
|
|
695
|
+
if (transcription instanceof EmptyTranscriptionError) {
|
|
696
|
+
await sendThreadMessage(thread, 'No request was transcribed. Record another voice message with the task to send.')
|
|
697
|
+
return transcription
|
|
698
|
+
}
|
|
683
699
|
const errMsg = errore.matchError(transcription, {
|
|
684
700
|
ApiKeyMissingError: (e) => e.message,
|
|
685
701
|
InvalidAudioFormatError: (e) => e.message,
|
|
686
702
|
TranscriptionError: (e) => e.message,
|
|
687
|
-
EmptyTranscriptionError: (e) => e.message,
|
|
688
703
|
NoResponseContentError: (e) => e.message,
|
|
689
704
|
NoToolResponseError: (e) => e.message,
|
|
690
705
|
Error: (e) => e.message,
|
|
@@ -728,6 +743,10 @@ export async function processVoiceAttachment({
|
|
|
728
743
|
thread,
|
|
729
744
|
`📝 **Transcribed message:** ${escapeDiscordFormatting(text)}`,
|
|
730
745
|
)
|
|
746
|
+
if (transcription.sessionAction === 'btw' && !canForkSession) {
|
|
747
|
+
await sendThreadMessage(thread, 'A contextual side chat needs an existing session. Nothing was sent to the agent. Retry from a session, or ask to create a fresh chat with this request.')
|
|
748
|
+
return new TranscriptionError({ reason: 'Contextual routing is unavailable without a source session' })
|
|
749
|
+
}
|
|
731
750
|
if (agent) {
|
|
732
751
|
await sendThreadMessage(thread, `Detected agent: ${agent}`)
|
|
733
752
|
}
|
|
@@ -28,6 +28,10 @@ import {
|
|
|
28
28
|
closeDatabase,
|
|
29
29
|
setChannelDirectory,
|
|
30
30
|
setChannelVerbosity,
|
|
31
|
+
getThreadSession,
|
|
32
|
+
createPendingWorkspace,
|
|
33
|
+
setWorkspaceReady,
|
|
34
|
+
getThreadWorktreeOrWorkspace,
|
|
31
35
|
} from './database.js'
|
|
32
36
|
import { startHranaServer, stopHranaServer } from './hrana-server.js'
|
|
33
37
|
import { initializeOpencodeForDirectory, getOpencodeClient, stopOpencodeServer } from './opencode.js'
|
|
@@ -429,6 +433,122 @@ e2eTest('voice message handling', () => {
|
|
|
429
433
|
|
|
430
434
|
// ── Test 1: Voice message in a channel creates thread + session ──
|
|
431
435
|
|
|
436
|
+
// Vitest disallows inline snapshots in test.each; register separate tests instead.
|
|
437
|
+
for (const sessionAction of ['btw', 'new-session'] as const) {
|
|
438
|
+
test(
|
|
439
|
+
`voice ${sessionAction} routing creates a separate thread with the correct history`,
|
|
440
|
+
async () => {
|
|
441
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID)
|
|
442
|
+
.sendMessage({ content: `Source history for voice ${sessionAction}` })
|
|
443
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
444
|
+
timeout: 4_000,
|
|
445
|
+
predicate: (candidate) => Boolean(candidate.name?.includes(`Source history for voice ${sessionAction}`)),
|
|
446
|
+
})
|
|
447
|
+
await waitForFooterMessage({ discord, threadId: thread.id, timeout: 4_000 })
|
|
448
|
+
const sourceSessionId = await getThreadSession(thread.id)
|
|
449
|
+
await createPendingWorkspace({
|
|
450
|
+
threadId: thread.id,
|
|
451
|
+
workspaceType: 'worktree',
|
|
452
|
+
workspaceName: 'voice-workspace',
|
|
453
|
+
projectDirectory: directories.projectDirectory,
|
|
454
|
+
})
|
|
455
|
+
await setWorkspaceReady({
|
|
456
|
+
threadId: thread.id,
|
|
457
|
+
workspaceId: 'voice-workspace-id',
|
|
458
|
+
workspaceDirectory: directories.projectDirectory,
|
|
459
|
+
})
|
|
460
|
+
const prompt = `Explain voice routing ${sessionAction}`
|
|
461
|
+
setDeterministicTranscription({ transcription: prompt, queueMessage: false, sessionAction })
|
|
462
|
+
await discord.thread(thread.id).user(TEST_USER_ID).sendVoiceMessage()
|
|
463
|
+
const confirmation = sessionAction === 'btw' ? 'Session forked!' : 'Created new session in'
|
|
464
|
+
const routingMessages = await waitForBotMessageContaining({
|
|
465
|
+
discord,
|
|
466
|
+
threadId: thread.id,
|
|
467
|
+
text: confirmation,
|
|
468
|
+
timeout: 4_000,
|
|
469
|
+
})
|
|
470
|
+
// OpenCode can rename the destination before discovery; follow its confirmed ID.
|
|
471
|
+
const targetId = routingMessages.find((message) => message.content.includes(confirmation))
|
|
472
|
+
?.content.match(/<#(\d+)>/)?.[1]
|
|
473
|
+
if (!targetId) throw new Error('Expected destination thread link')
|
|
474
|
+
const target = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
475
|
+
timeout: 4_000,
|
|
476
|
+
predicate: (candidate) => candidate.id === targetId,
|
|
477
|
+
})
|
|
478
|
+
await waitForFooterMessage({ discord, threadId: target.id, timeout: 4_000 })
|
|
479
|
+
const th = discord.thread(target.id)
|
|
480
|
+
const transcript = {
|
|
481
|
+
source: (await discord.thread(thread.id).text()).replaceAll(target.id, 'TARGET_THREAD'),
|
|
482
|
+
target: (await th.text()).replaceAll(thread.id, 'SOURCE_THREAD'),
|
|
483
|
+
}
|
|
484
|
+
if (sessionAction === 'btw') {
|
|
485
|
+
expect(transcript).toMatchInlineSnapshot(`
|
|
486
|
+
{
|
|
487
|
+
"source": "--- from: user (voice-tester)
|
|
488
|
+
Source history for voice btw
|
|
489
|
+
--- from: assistant (TestBot)
|
|
490
|
+
> *using deterministic-provider/deterministic-v2*
|
|
491
|
+
> session-reply
|
|
492
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>
|
|
493
|
+
--- from: user (voice-tester)
|
|
494
|
+
[attachment: voice-message.ogg]
|
|
495
|
+
--- from: assistant (TestBot)
|
|
496
|
+
🎤 Transcribing voice message...
|
|
497
|
+
📝 **Transcribed message:** Explain voice routing btw
|
|
498
|
+
Session forked! Continue in <#TARGET_THREAD>",
|
|
499
|
+
"target": "--- from: assistant (TestBot)
|
|
500
|
+
Reusing context from <#SOURCE_THREAD> to answer prompt...
|
|
501
|
+
Explain voice routing btw
|
|
502
|
+
> session-reply
|
|
503
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>",
|
|
504
|
+
}
|
|
505
|
+
`)
|
|
506
|
+
} else {
|
|
507
|
+
expect(transcript).toMatchInlineSnapshot(`
|
|
508
|
+
{
|
|
509
|
+
"source": "--- from: user (voice-tester)
|
|
510
|
+
Source history for voice new-session
|
|
511
|
+
--- from: assistant (TestBot)
|
|
512
|
+
> *using deterministic-provider/deterministic-v2*
|
|
513
|
+
> session-reply
|
|
514
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>
|
|
515
|
+
--- from: user (voice-tester)
|
|
516
|
+
[attachment: voice-message.ogg]
|
|
517
|
+
--- from: assistant (TestBot)
|
|
518
|
+
🎤 Transcribing voice message...
|
|
519
|
+
📝 **Transcribed message:** Explain voice routing new-session
|
|
520
|
+
Created new session in <#TARGET_THREAD>",
|
|
521
|
+
"target": "--- from: assistant (TestBot)
|
|
522
|
+
**Starting OpenCode session**
|
|
523
|
+
Explain voice routing new-session
|
|
524
|
+
> *using deterministic-provider/deterministic-v2*
|
|
525
|
+
> session-reply
|
|
526
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>",
|
|
527
|
+
}
|
|
528
|
+
`)
|
|
529
|
+
}
|
|
530
|
+
const targetSessionId = await getThreadSession(target.id)
|
|
531
|
+
expect(targetSessionId).toBeTruthy()
|
|
532
|
+
expect(targetSessionId).not.toBe(sourceSessionId)
|
|
533
|
+
expect(await getThreadWorktreeOrWorkspace(target.id)).toMatchObject({
|
|
534
|
+
status: 'ready',
|
|
535
|
+
workspace_type: 'worktree',
|
|
536
|
+
workspace_name: 'voice-workspace',
|
|
537
|
+
workspace_id: 'voice-workspace-id',
|
|
538
|
+
workspace_directory: directories.projectDirectory,
|
|
539
|
+
})
|
|
540
|
+
const client = getOpencodeClientForTest(directories.projectDirectory)
|
|
541
|
+
const targetMessages = await client.session.messages({ sessionID: targetSessionId! })
|
|
542
|
+
const targetTexts = getUserTexts(targetMessages.data ?? []).join('\n')
|
|
543
|
+
expect(targetTexts).toContain(prompt)
|
|
544
|
+
expect(targetTexts).toContain(`Voice message transcription from Discord user:\n${prompt}`)
|
|
545
|
+
expect(targetTexts.includes(`Source history for voice ${sessionAction}`)).toBe(sessionAction === 'btw')
|
|
546
|
+
const sourceMessages = await client.session.messages({ sessionID: sourceSessionId! })
|
|
547
|
+
expect(getUserTexts(sourceMessages.data ?? []).join('\n')).not.toContain(prompt)
|
|
548
|
+
},
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
|
|
432
552
|
test(
|
|
433
553
|
'voice message in channel creates thread and starts session',
|
|
434
554
|
async () => {
|
|
@@ -503,9 +623,9 @@ e2eTest('voice message handling', () => {
|
|
|
503
623
|
--- from: assistant (TestBot)
|
|
504
624
|
🎤 Transcribing voice message...
|
|
505
625
|
📝 **Transcribed message:** Fix the login bug in auth.ts
|
|
506
|
-
*using deterministic-provider/deterministic-v2*
|
|
507
|
-
|
|
508
|
-
*project ⋅ main ⋅
|
|
626
|
+
> *using deterministic-provider/deterministic-v2*
|
|
627
|
+
> session-reply
|
|
628
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
|
|
509
629
|
`)
|
|
510
630
|
expect(finalState.sessionId).toBeDefined()
|
|
511
631
|
|
|
@@ -607,9 +727,9 @@ e2eTest('voice message handling', () => {
|
|
|
607
727
|
Voice transcription requires an API key (OpenAI or Gemini). Set one to enable voice message transcription.
|
|
608
728
|
Gemini API key saved. Retrying the original voice message.
|
|
609
729
|
📝 **Transcribed message:** Resume the original voice note
|
|
610
|
-
*using deterministic-provider/deterministic-v2*
|
|
611
|
-
|
|
612
|
-
*project ⋅ main ⋅
|
|
730
|
+
> *using deterministic-provider/deterministic-v2*
|
|
731
|
+
> session-reply
|
|
732
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
|
|
613
733
|
`)
|
|
614
734
|
|
|
615
735
|
const finalState = getThreadState(thread.id)
|
|
@@ -685,9 +805,9 @@ e2eTest('voice message handling', () => {
|
|
|
685
805
|
--- from: assistant (TestBot)
|
|
686
806
|
🎤 Transcribing voice message...
|
|
687
807
|
📝 **Transcribed message:** Investigate the missing content type path
|
|
688
|
-
*using deterministic-provider/deterministic-v2*
|
|
689
|
-
|
|
690
|
-
*project ⋅ main ⋅
|
|
808
|
+
> *using deterministic-provider/deterministic-v2*
|
|
809
|
+
> session-reply
|
|
810
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
|
|
691
811
|
`)
|
|
692
812
|
|
|
693
813
|
const messages = await waitForSessionMessages({
|
|
@@ -794,16 +914,16 @@ e2eTest('voice message handling', () => {
|
|
|
794
914
|
"--- from: user (voice-tester)
|
|
795
915
|
FAST_RESPONSE_MARKER initial setup
|
|
796
916
|
--- from: assistant (TestBot)
|
|
797
|
-
*using deterministic-provider/deterministic-v2*
|
|
798
|
-
|
|
799
|
-
*project ⋅ main ⋅
|
|
917
|
+
> *using deterministic-provider/deterministic-v2*
|
|
918
|
+
> fast-response-done
|
|
919
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>
|
|
800
920
|
--- from: user (voice-tester)
|
|
801
921
|
[attachment: voice-message.ogg]
|
|
802
922
|
--- from: assistant (TestBot)
|
|
803
923
|
🎤 Transcribing voice message...
|
|
804
924
|
📝 **Transcribed message:** Add error handling to the parser
|
|
805
|
-
|
|
806
|
-
*project ⋅ main ⋅
|
|
925
|
+
> session-reply
|
|
926
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
|
|
807
927
|
`)
|
|
808
928
|
expect(finalState?.sessionId).toBeDefined()
|
|
809
929
|
if (!finalState?.sessionId) {
|
|
@@ -1053,19 +1173,19 @@ e2eTest('voice message handling', () => {
|
|
|
1053
1173
|
"--- from: user (voice-tester)
|
|
1054
1174
|
SLOW_RESPONSE_MARKER start queued task
|
|
1055
1175
|
--- from: assistant (TestBot)
|
|
1056
|
-
*using deterministic-provider/deterministic-v2*
|
|
1176
|
+
> *using deterministic-provider/deterministic-v2*
|
|
1057
1177
|
--- from: user (voice-tester)
|
|
1058
1178
|
[attachment: voice-message.ogg]
|
|
1059
1179
|
--- from: assistant (TestBot)
|
|
1060
1180
|
🎤 Transcribing voice message...
|
|
1061
1181
|
📝 **Transcribed message:** Queue this task for later
|
|
1062
1182
|
Queued at position 1. Edit or delete your message to update the queue
|
|
1063
|
-
|
|
1064
|
-
*project ⋅ main ⋅
|
|
1183
|
+
> slow-response-done
|
|
1184
|
+
> *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2*
|
|
1065
1185
|
» **voice-tester:** Voice message transcription from Discord user:
|
|
1066
1186
|
Queue this task for later
|
|
1067
|
-
|
|
1068
|
-
*project ⋅ main ⋅
|
|
1187
|
+
> session-reply
|
|
1188
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
|
|
1069
1189
|
`)
|
|
1070
1190
|
expect(finalState.queueItems.length).toBe(0)
|
|
1071
1191
|
|
|
@@ -1181,16 +1301,16 @@ e2eTest('voice message handling', () => {
|
|
|
1181
1301
|
"--- from: user (voice-tester)
|
|
1182
1302
|
FAST_RESPONSE_MARKER quick task
|
|
1183
1303
|
--- from: assistant (TestBot)
|
|
1184
|
-
*using deterministic-provider/deterministic-v2*
|
|
1185
|
-
|
|
1186
|
-
*project ⋅ main ⋅
|
|
1304
|
+
> *using deterministic-provider/deterministic-v2*
|
|
1305
|
+
> fast-response-done
|
|
1306
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>
|
|
1187
1307
|
--- from: user (voice-tester)
|
|
1188
1308
|
[attachment: voice-message.ogg]
|
|
1189
1309
|
--- from: assistant (TestBot)
|
|
1190
1310
|
🎤 Transcribing voice message...
|
|
1191
1311
|
📝 **Transcribed message:** Delayed transcription result
|
|
1192
|
-
|
|
1193
|
-
*project ⋅ main ⋅
|
|
1312
|
+
> session-reply
|
|
1313
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
|
|
1194
1314
|
`)
|
|
1195
1315
|
expect(finalState.sessionId).toBeDefined()
|
|
1196
1316
|
expect(finalState.queueItems.length).toBe(0)
|
|
@@ -1319,16 +1439,16 @@ e2eTest('voice message handling', () => {
|
|
|
1319
1439
|
"--- from: user (voice-tester)
|
|
1320
1440
|
FAST_RESPONSE_MARKER fast before queued voice
|
|
1321
1441
|
--- from: assistant (TestBot)
|
|
1322
|
-
*using deterministic-provider/deterministic-v2*
|
|
1323
|
-
|
|
1324
|
-
*project ⋅ main ⋅
|
|
1442
|
+
> *using deterministic-provider/deterministic-v2*
|
|
1443
|
+
> fast-response-done
|
|
1444
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>
|
|
1325
1445
|
--- from: user (voice-tester)
|
|
1326
1446
|
[attachment: voice-message.ogg]
|
|
1327
1447
|
--- from: assistant (TestBot)
|
|
1328
1448
|
🎤 Transcribing voice message...
|
|
1329
1449
|
📝 **Transcribed message:** Queued voice after idle
|
|
1330
|
-
|
|
1331
|
-
*project ⋅ main ⋅
|
|
1450
|
+
> session-reply
|
|
1451
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
|
|
1332
1452
|
`)
|
|
1333
1453
|
expect(finalState.sessionId).toBeDefined()
|
|
1334
1454
|
expect(finalState.queueItems.length).toBe(0)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Cover long routed prompts and rejected voice routes without an existing session.
|
|
2
|
+
import { afterEach, describe, expect, test } from 'vitest'
|
|
3
|
+
import { ChannelType } from 'discord.js'
|
|
4
|
+
import { setupQueueAdvancedSuite, TEST_USER_ID } from './queue-advanced-e2e-setup.js'
|
|
5
|
+
import { waitForBotMessageContaining, waitForFooterMessage } from './test-utils.js'
|
|
6
|
+
import { getThreadSession } from './database.js'
|
|
7
|
+
import { getOpencodeClient } from './opencode.js'
|
|
8
|
+
import { store } from './store.js'
|
|
9
|
+
|
|
10
|
+
const TEXT_CHANNEL_ID = '200000000000001084'
|
|
11
|
+
|
|
12
|
+
describe('voice routing input boundaries', () => {
|
|
13
|
+
const ctx = setupQueueAdvancedSuite({
|
|
14
|
+
channelId: TEXT_CHANNEL_ID,
|
|
15
|
+
channelName: 'voice-routing-input',
|
|
16
|
+
dirName: 'voice-routing-input',
|
|
17
|
+
username: 'voice-input-tester',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
store.setState({ test: { deterministicTranscription: null } })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('fresh voice route splits display and preserves the full model prompt', async () => {
|
|
25
|
+
const channel = await ctx.botClient.channels.fetch(TEXT_CHANNEL_ID)
|
|
26
|
+
if (channel?.type !== ChannelType.GuildText) throw new Error('Expected text channel')
|
|
27
|
+
const source = await channel.threads.create({ name: 'Long voice source' })
|
|
28
|
+
const prompt = `Long voice request ${'Z'.repeat(2100)} END_OF_REQUEST`
|
|
29
|
+
const caption = 'Keep the existing public API unchanged.'
|
|
30
|
+
store.setState({
|
|
31
|
+
test: {
|
|
32
|
+
deterministicTranscription: {
|
|
33
|
+
transcription: prompt,
|
|
34
|
+
queueMessage: false,
|
|
35
|
+
sessionAction: 'new-session',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
await ctx.discord.thread(source.id).user(TEST_USER_ID).sendVoiceMessage({ content: caption })
|
|
40
|
+
const routingMessages = await waitForBotMessageContaining({
|
|
41
|
+
discord: ctx.discord,
|
|
42
|
+
threadId: source.id,
|
|
43
|
+
text: 'Created new session in',
|
|
44
|
+
timeout: 4_000,
|
|
45
|
+
})
|
|
46
|
+
const targetId = routingMessages.find((message) => message.content.includes('Created new session in'))
|
|
47
|
+
?.content.match(/<#(\d+)>/)?.[1]
|
|
48
|
+
if (!targetId) throw new Error('Expected destination thread link')
|
|
49
|
+
const target = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
50
|
+
timeout: 4_000,
|
|
51
|
+
predicate: (thread) => thread.id === targetId,
|
|
52
|
+
})
|
|
53
|
+
await waitForFooterMessage({ discord: ctx.discord, threadId: target.id, timeout: 4_000 })
|
|
54
|
+
expect((await ctx.discord.channel(TEXT_CHANNEL_ID).text()).replace(/Z+/g, '[DETAILS]'))
|
|
55
|
+
.toMatchInlineSnapshot(`
|
|
56
|
+
"--- from: assistant (TestBot)
|
|
57
|
+
**Starting OpenCode session**
|
|
58
|
+
Keep the existing public API unchanged.
|
|
59
|
+
|
|
60
|
+
"
|
|
61
|
+
`)
|
|
62
|
+
expect((await ctx.discord.thread(target.id).text()).replace(/Z+/g, '[DETAILS]'))
|
|
63
|
+
.toMatchInlineSnapshot(`
|
|
64
|
+
"--- from: assistant (TestBot)
|
|
65
|
+
**Starting OpenCode session**
|
|
66
|
+
Keep the existing public API unchanged.
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
Long voice request [DETAILS]
|
|
70
|
+
[DETAILS] END_OF_REQUEST
|
|
71
|
+
> *using deterministic-provider/deterministic-v2*
|
|
72
|
+
> ok
|
|
73
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
74
|
+
`)
|
|
75
|
+
const channelMessages = await ctx.discord.channel(TEXT_CHANNEL_ID).getMessages()
|
|
76
|
+
expect(channelMessages).toHaveLength(1)
|
|
77
|
+
expect(channelMessages.every((message) => message.content.length <= 2000)).toBe(true)
|
|
78
|
+
const threadText = await ctx.discord.thread(target.id).text()
|
|
79
|
+
expect(threadText).toContain(caption)
|
|
80
|
+
expect(threadText.replace(/\s/g, '')).toContain(prompt.replace(/\s/g, ''))
|
|
81
|
+
expect(
|
|
82
|
+
threadText.match(/Z/g),
|
|
83
|
+
).toHaveLength(2100)
|
|
84
|
+
const sessionId = await getThreadSession(target.id)
|
|
85
|
+
if (!sessionId) throw new Error('Expected target session')
|
|
86
|
+
const client = getOpencodeClient(ctx.directories.projectDirectory)
|
|
87
|
+
if (!client) throw new Error('Expected OpenCode client')
|
|
88
|
+
const messages = await client.session.messages({ sessionID: sessionId })
|
|
89
|
+
const userTexts = messages.data
|
|
90
|
+
?.filter((message) => message.info.role === 'user')
|
|
91
|
+
.flatMap((message) =>
|
|
92
|
+
message.parts.flatMap((part) => (part.type === 'text' ? [part.text] : [])),
|
|
93
|
+
)
|
|
94
|
+
expect(userTexts?.join('\n')).toContain(
|
|
95
|
+
`Voice message transcription from Discord user:\n${prompt}`,
|
|
96
|
+
)
|
|
97
|
+
expect(userTexts?.join('\n')).toContain(caption)
|
|
98
|
+
expect(await getThreadSession(source.id)).toBeFalsy()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('unsupported contextual route keeps the transcript and never dispatches raw text', async () => {
|
|
102
|
+
const channel = await ctx.botClient.channels.fetch(TEXT_CHANNEL_ID)
|
|
103
|
+
if (channel?.type !== ChannelType.GuildText) throw new Error('Expected text channel')
|
|
104
|
+
const source = await channel.threads.create({ name: 'Unsupported voice source' })
|
|
105
|
+
const th = ctx.discord.thread(source.id)
|
|
106
|
+
store.setState({
|
|
107
|
+
test: {
|
|
108
|
+
deterministicTranscription: {
|
|
109
|
+
transcription: 'Explain the parser in a contextual side chat',
|
|
110
|
+
queueMessage: false,
|
|
111
|
+
sessionAction: 'btw',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
await th.user(TEST_USER_ID).sendVoiceMessage({ content: 'Do not dispatch this raw text' })
|
|
116
|
+
await waitForBotMessageContaining({
|
|
117
|
+
discord: ctx.discord,
|
|
118
|
+
threadId: source.id,
|
|
119
|
+
text: 'Nothing was sent to the agent',
|
|
120
|
+
timeout: 4_000,
|
|
121
|
+
})
|
|
122
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
123
|
+
"--- from: user (voice-input-tester)
|
|
124
|
+
Do not dispatch this raw text
|
|
125
|
+
[attachment: voice-message.ogg]
|
|
126
|
+
--- from: assistant (TestBot)
|
|
127
|
+
🎤 Transcribing voice message...
|
|
128
|
+
📝 **Transcribed message:** Explain the parser in a contextual side chat
|
|
129
|
+
A contextual side chat needs an existing session. Nothing was sent to the agent. Retry from a session, or ask to create a fresh chat with this request."
|
|
130
|
+
`)
|
|
131
|
+
for (let index = 0; index < 10; index++) {
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
133
|
+
expect(await getThreadSession(source.id)).toBeFalsy()
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('routing-only audio does not start a junk turn', async () => {
|
|
138
|
+
const channel = await ctx.botClient.channels.fetch(TEXT_CHANNEL_ID)
|
|
139
|
+
if (channel?.type !== ChannelType.GuildText) throw new Error('Expected text channel')
|
|
140
|
+
const source = await channel.threads.create({ name: 'Empty voice source' })
|
|
141
|
+
const th = ctx.discord.thread(source.id)
|
|
142
|
+
store.setState({
|
|
143
|
+
test: {
|
|
144
|
+
deterministicTranscription: {
|
|
145
|
+
transcription: '',
|
|
146
|
+
queueMessage: false,
|
|
147
|
+
sessionAction: 'new-session',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
await th.user(TEST_USER_ID).sendVoiceMessage({ content: 'Create a new chat' })
|
|
152
|
+
await waitForBotMessageContaining({
|
|
153
|
+
discord: ctx.discord,
|
|
154
|
+
threadId: source.id,
|
|
155
|
+
text: 'No request was transcribed',
|
|
156
|
+
timeout: 4_000,
|
|
157
|
+
})
|
|
158
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
159
|
+
"--- from: user (voice-input-tester)
|
|
160
|
+
Create a new chat
|
|
161
|
+
[attachment: voice-message.ogg]
|
|
162
|
+
--- from: assistant (TestBot)
|
|
163
|
+
🎤 Transcribing voice message...
|
|
164
|
+
No request was transcribed. Record another voice message with the task to send."
|
|
165
|
+
`)
|
|
166
|
+
for (let index = 0; index < 10; index++) {
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
168
|
+
expect(await getThreadSession(source.id)).toBeFalsy()
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('/new-session confirms the destination and sends the prompt', async () => {
|
|
173
|
+
const channel = await ctx.botClient.channels.fetch(TEXT_CHANNEL_ID)
|
|
174
|
+
if (channel?.type !== ChannelType.GuildText) throw new Error('Expected text channel')
|
|
175
|
+
const source = await channel.threads.create({ name: 'Slash source' })
|
|
176
|
+
const th = ctx.discord.thread(source.id)
|
|
177
|
+
const interaction = await th.user(TEST_USER_ID).runSlashCommand({
|
|
178
|
+
name: 'new-session',
|
|
179
|
+
options: [{ name: 'prompt', type: 3, value: 'Slash fresh request' }],
|
|
180
|
+
})
|
|
181
|
+
await th.waitForInteractionAck({ interactionId: interaction.id, timeout: 4_000 })
|
|
182
|
+
const target = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
183
|
+
timeout: 4_000,
|
|
184
|
+
predicate: (thread) => thread.name === 'Slash fresh request',
|
|
185
|
+
})
|
|
186
|
+
await waitForFooterMessage({ discord: ctx.discord, threadId: target.id, timeout: 4_000 })
|
|
187
|
+
expect(
|
|
188
|
+
(await th.text({ showInteractions: true })).replaceAll(target.id, 'TARGET_THREAD'),
|
|
189
|
+
).toMatchInlineSnapshot(`
|
|
190
|
+
"[user interaction]
|
|
191
|
+
--- from: assistant (TestBot)
|
|
192
|
+
Created new session in <#TARGET_THREAD>"
|
|
193
|
+
`)
|
|
194
|
+
expect(await ctx.discord.thread(target.id).text()).toMatchInlineSnapshot(`
|
|
195
|
+
"--- from: assistant (TestBot)
|
|
196
|
+
**Starting OpenCode session**
|
|
197
|
+
Slash fresh request
|
|
198
|
+
> *using deterministic-provider/deterministic-v2*
|
|
199
|
+
> ok
|
|
200
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
201
|
+
`)
|
|
202
|
+
})
|
|
203
|
+
})
|