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
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
import { describe, test, expect, afterEach } from 'vitest'
|
|
7
7
|
import { setupQueueAdvancedSuite, TEST_USER_ID } from './queue-advanced-e2e-setup.js'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
waitForBotMessageContaining,
|
|
10
|
+
waitForBotReplyAfterUserMessage,
|
|
11
|
+
waitForFooterMessage,
|
|
12
|
+
} from './test-utils.js'
|
|
9
13
|
import { store, type DeterministicTranscriptionConfig } from './store.js'
|
|
10
14
|
import { getOpencodeClient } from './opencode.js'
|
|
11
15
|
import { getThreadSession } from './database.js'
|
|
12
16
|
import type { Message, Part } from '@opencode-ai/sdk/v2'
|
|
13
17
|
|
|
14
18
|
const TEXT_CHANNEL_ID = '200000000000001007'
|
|
15
|
-
const VOICE_CHANNEL_ID = '200000000000001017'
|
|
16
19
|
|
|
17
20
|
function setDeterministicTranscription(config: DeterministicTranscriptionConfig | null) {
|
|
18
21
|
store.setState({
|
|
@@ -158,37 +161,44 @@ describe('queue advanced: question tool answer', () => {
|
|
|
158
161
|
// 1. Dismiss the pending question (cleanup context)
|
|
159
162
|
// 2. Abort the blocked session so OpenCode unblocks
|
|
160
163
|
// 3. Enqueue the message as a normal user prompt (not consumed as answer)
|
|
164
|
+
const answer = 'Reply with exactly: question-text-answer-done'
|
|
161
165
|
await th.user(TEST_USER_ID).sendMessage({
|
|
162
|
-
content:
|
|
166
|
+
content: answer,
|
|
163
167
|
})
|
|
164
168
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
await waitForBotReplyAfterUserMessage({
|
|
170
|
+
discord: ctx.discord,
|
|
171
|
+
threadId: thread.id,
|
|
172
|
+
userId: TEST_USER_ID,
|
|
173
|
+
userMessageIncludes: answer,
|
|
174
|
+
timeout: 8_000,
|
|
175
|
+
})
|
|
176
|
+
await waitForBotMessageContaining({
|
|
177
|
+
discord: ctx.discord,
|
|
178
|
+
threadId: thread.id,
|
|
179
|
+
userId: TEST_USER_ID,
|
|
180
|
+
text: 'ok',
|
|
181
|
+
afterUserMessageIncludes: answer,
|
|
182
|
+
timeout: 8_000,
|
|
183
|
+
})
|
|
184
|
+
await waitForFooterMessage({
|
|
185
|
+
discord: ctx.discord,
|
|
186
|
+
threadId: thread.id,
|
|
187
|
+
timeout: 8_000,
|
|
188
|
+
afterMessageIncludes: 'ok',
|
|
189
|
+
afterAuthorId: ctx.discord.botUserId,
|
|
168
190
|
})
|
|
169
191
|
|
|
170
192
|
const timeline = await th.text({ showInteractions: true })
|
|
171
193
|
|
|
172
194
|
// The user's text answer must appear in Discord
|
|
173
|
-
expect(timeline).toContain(
|
|
195
|
+
expect(timeline).toContain(answer)
|
|
196
|
+
expect(timeline).toContain('ok')
|
|
174
197
|
// The original question must have appeared
|
|
175
198
|
expect(timeline).toContain('Which option do you prefer?')
|
|
176
199
|
// The user's marker message triggered the question
|
|
177
200
|
expect(timeline).toContain('QUESTION_TEXT_ANSWER_MARKER')
|
|
178
201
|
}, 20_000)
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
describe('queue advanced: voice message during pending question', () => {
|
|
182
|
-
const ctx = setupQueueAdvancedSuite({
|
|
183
|
-
channelId: VOICE_CHANNEL_ID,
|
|
184
|
-
channelName: 'qa-question-voice-e2e',
|
|
185
|
-
dirName: 'qa-question-voice-e2e',
|
|
186
|
-
username: 'queue-question-tester',
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
afterEach(() => {
|
|
190
|
-
setDeterministicTranscription(null)
|
|
191
|
-
})
|
|
192
202
|
|
|
193
203
|
test('voice message during pending question dismisses question and transcribes normally', async () => {
|
|
194
204
|
// This is the exact bug scenario: user sends a voice message while a
|
|
@@ -196,14 +206,15 @@ describe('queue advanced: voice message during pending question', () => {
|
|
|
196
206
|
// (audio is in attachments, transcription happens later). The old code
|
|
197
207
|
// passed "" as the question answer and consumed the message — the voice
|
|
198
208
|
// content was completely lost.
|
|
199
|
-
|
|
200
|
-
|
|
209
|
+
const marker = 'QUESTION_TEXT_ANSWER_MARKER voice'
|
|
210
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
211
|
+
content: marker,
|
|
201
212
|
})
|
|
202
213
|
|
|
203
|
-
const thread = await ctx.discord.channel(
|
|
214
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
204
215
|
timeout: 8_000,
|
|
205
216
|
predicate: (t) => {
|
|
206
|
-
return t.name ===
|
|
217
|
+
return t.name === marker
|
|
207
218
|
},
|
|
208
219
|
})
|
|
209
220
|
|
|
@@ -230,11 +241,6 @@ describe('queue advanced: voice message during pending question', () => {
|
|
|
230
241
|
content: 'VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL',
|
|
231
242
|
})
|
|
232
243
|
|
|
233
|
-
// Give time for question cleanup to propagate
|
|
234
|
-
await new Promise((r) => {
|
|
235
|
-
setTimeout(r, 1_000)
|
|
236
|
-
})
|
|
237
|
-
|
|
238
244
|
// Voice content should be transcribed and appear as the next user message,
|
|
239
245
|
// processed after the model responds to the empty question answer.
|
|
240
246
|
await waitForBotMessageContaining({
|
|
@@ -308,7 +314,7 @@ describe('queue advanced: voice message during pending question', () => {
|
|
|
308
314
|
expect(timeline).toContain('VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL')
|
|
309
315
|
expect(timeline).toContain('🎤 Transcribing voice message...')
|
|
310
316
|
expect(timeline).toContain('📝 **Transcribed message:** I want option Alpha please')
|
|
311
|
-
expect(timeline).toContain('
|
|
317
|
+
expect(timeline).toContain('ok')
|
|
312
318
|
|
|
313
319
|
// Voice content must be present as a real transcribed message, not lost
|
|
314
320
|
expect(timeline).toContain('I want option Alpha please')
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
TEST_USER_ID,
|
|
8
8
|
} from './queue-advanced-e2e-setup.js'
|
|
9
9
|
import {
|
|
10
|
+
getMessageVisibleText,
|
|
11
|
+
isFooterMessage,
|
|
10
12
|
waitForBotMessageContaining,
|
|
11
13
|
waitForFooterMessage,
|
|
12
14
|
} from './test-utils.js'
|
|
@@ -91,33 +93,31 @@ e2eTest('queue advanced: typing interrupt', () => {
|
|
|
91
93
|
if (index <= finalUserIndex) {
|
|
92
94
|
return false
|
|
93
95
|
}
|
|
94
|
-
return message.author.id === ctx.discord.botUserId && message.
|
|
96
|
+
return message.author.id === ctx.discord.botUserId && getMessageVisibleText(message).includes('ok')
|
|
95
97
|
})
|
|
96
98
|
const finalFooterIndex = messages.findIndex((message, index) => {
|
|
97
99
|
if (index <= finalReplyIndex) {
|
|
98
100
|
return false
|
|
99
101
|
}
|
|
100
|
-
return message
|
|
101
|
-
&& message.content.startsWith('*')
|
|
102
|
-
&& message.content.includes('⋅')
|
|
102
|
+
return isFooterMessage({ message, botUserId: ctx.discord.botUserId })
|
|
103
103
|
})
|
|
104
104
|
|
|
105
105
|
expect(await th.text()).toMatchInlineSnapshot(`
|
|
106
106
|
"--- from: user (queue-advanced-tester)
|
|
107
107
|
Reply with exactly: typing-stop-interrupt-setup
|
|
108
108
|
--- from: assistant (TestBot)
|
|
109
|
-
*using deterministic-provider/deterministic-v2*
|
|
110
|
-
|
|
111
|
-
*project ⋅ main ⋅
|
|
109
|
+
> *using deterministic-provider/deterministic-v2*
|
|
110
|
+
> ok
|
|
111
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
|
|
112
112
|
--- from: user (queue-advanced-tester)
|
|
113
113
|
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
114
114
|
--- from: assistant (TestBot)
|
|
115
|
-
|
|
115
|
+
> starting sleep 100
|
|
116
116
|
--- from: user (queue-advanced-tester)
|
|
117
117
|
Reply with exactly: typing-stop-interrupt-final
|
|
118
118
|
--- from: assistant (TestBot)
|
|
119
|
-
|
|
120
|
-
*project ⋅ main ⋅
|
|
119
|
+
> ok
|
|
120
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
121
121
|
`)
|
|
122
122
|
|
|
123
123
|
const timeline = await th.text({ showTyping: true })
|
|
@@ -129,7 +129,7 @@ e2eTest('queue advanced: typing interrupt', () => {
|
|
|
129
129
|
const finalPromptPosition = timeline.indexOf(
|
|
130
130
|
'Reply with exactly: typing-stop-interrupt-final',
|
|
131
131
|
)
|
|
132
|
-
const finalReplyPosition = timeline.indexOf('--- from: assistant (TestBot)\n
|
|
132
|
+
const finalReplyPosition = timeline.indexOf('--- from: assistant (TestBot)\n> ok', finalPromptPosition)
|
|
133
133
|
const lastFooterPosition = timeline.lastIndexOf('*project ⋅')
|
|
134
134
|
expect(finalPromptPosition).toBeGreaterThanOrEqual(0)
|
|
135
135
|
expect(finalReplyPosition).toBeGreaterThan(finalPromptPosition)
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
TEST_USER_ID,
|
|
8
8
|
} from './queue-advanced-e2e-setup.js'
|
|
9
9
|
import {
|
|
10
|
+
getMessageVisibleText,
|
|
11
|
+
isFooterMessage,
|
|
10
12
|
waitForBotMessageContaining,
|
|
11
13
|
waitForFooterMessage,
|
|
12
14
|
} from './test-utils.js'
|
|
@@ -60,20 +62,18 @@ e2eTest('queue advanced: typing lifecycle', () => {
|
|
|
60
62
|
})
|
|
61
63
|
|
|
62
64
|
const replyIndex = messages.findIndex((message) => {
|
|
63
|
-
return message.author.id === ctx.discord.botUserId && message.
|
|
65
|
+
return message.author.id === ctx.discord.botUserId && getMessageVisibleText(message).includes('ok')
|
|
64
66
|
})
|
|
65
67
|
const footerIndex = messages.findIndex((message, index) => {
|
|
66
68
|
if (index <= replyIndex) {
|
|
67
69
|
return false
|
|
68
70
|
}
|
|
69
|
-
return message
|
|
70
|
-
&& message.content.startsWith('*')
|
|
71
|
-
&& message.content.includes('⋅')
|
|
71
|
+
return isFooterMessage({ message, botUserId: ctx.discord.botUserId })
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
const timeline = await th.text({ showTyping: true })
|
|
75
75
|
expect(timeline).toContain('Reply with exactly: typing-stop-normal')
|
|
76
|
-
expect(timeline).toContain('
|
|
76
|
+
expect(timeline).toContain('ok')
|
|
77
77
|
expect(timeline).toContain('*project ⋅ main ⋅')
|
|
78
78
|
const typingCount = (timeline.match(/\[bot typing\]/g) || []).length
|
|
79
79
|
expect(typingCount).toBeGreaterThanOrEqual(1)
|
|
@@ -145,7 +145,7 @@ e2eTest('queue advanced: typing lifecycle', () => {
|
|
|
145
145
|
return false
|
|
146
146
|
}
|
|
147
147
|
return message.author.id === ctx.discord.botUserId
|
|
148
|
-
&& message.
|
|
148
|
+
&& getMessageVisibleText(message).includes('repulse-first')
|
|
149
149
|
})
|
|
150
150
|
if (!firstReply) {
|
|
151
151
|
throw new Error('Expected first bot reply after TYPING_REPULSE_MARKER')
|
|
@@ -173,7 +173,7 @@ e2eTest('queue advanced: typing lifecycle', () => {
|
|
|
173
173
|
|
|
174
174
|
const timeline = await th.text({ showTyping: true })
|
|
175
175
|
expect(timeline).toContain('TYPING_REPULSE_MARKER')
|
|
176
|
-
expect(timeline).toContain('
|
|
176
|
+
expect(timeline).toContain('repulse-first')
|
|
177
177
|
const typingCount = (timeline.match(/\[bot typing\]/g) || []).length
|
|
178
178
|
expect(typingCount).toBeGreaterThanOrEqual(2)
|
|
179
179
|
|
|
@@ -186,7 +186,7 @@ e2eTest('queue advanced: typing lifecycle', () => {
|
|
|
186
186
|
return false
|
|
187
187
|
}
|
|
188
188
|
return message.author.id === ctx.discord.botUserId
|
|
189
|
-
&& message.
|
|
189
|
+
&& getMessageVisibleText(message).includes('repulse-first')
|
|
190
190
|
})
|
|
191
191
|
|
|
192
192
|
expect(followupUserIndex).toBeGreaterThanOrEqual(0)
|
|
@@ -86,11 +86,11 @@ e2eTest('queue delete message', () => {
|
|
|
86
86
|
"--- from: user (queue-delete-tester)
|
|
87
87
|
SLOW_BUSY_MARKER Reply with exactly: delete-queue-setup
|
|
88
88
|
--- from: assistant (TestBot)
|
|
89
|
-
*using deterministic-provider/deterministic-v2*
|
|
89
|
+
> *using deterministic-provider/deterministic-v2*
|
|
90
90
|
Queued at position 1. Edit or delete your message to update the queue
|
|
91
91
|
⬦ **queue-delete-tester** removed message from queue
|
|
92
|
-
|
|
93
|
-
*project ⋅ main ⋅
|
|
92
|
+
> slow-busy-reply
|
|
93
|
+
> *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
94
94
|
`)
|
|
95
95
|
const finalText = await th.text()
|
|
96
96
|
expect(finalText).not.toContain(
|
|
@@ -137,14 +137,14 @@ describe('queue drain with pending interactive UI', () => {
|
|
|
137
137
|
"--- from: user (drain-ui-tester)
|
|
138
138
|
Reply with exactly: drain-button-setup
|
|
139
139
|
--- from: assistant (TestBot)
|
|
140
|
-
*using deterministic-provider/deterministic-v2*
|
|
141
|
-
|
|
142
|
-
*project ⋅ main ⋅
|
|
140
|
+
> *using deterministic-provider/deterministic-v2*
|
|
141
|
+
> ok
|
|
142
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
|
|
143
143
|
**Action Required**
|
|
144
144
|
[user interaction]
|
|
145
145
|
» **drain-ui-tester:** Reply with exactly: post-button-drain
|
|
146
|
-
|
|
147
|
-
*project ⋅ main ⋅
|
|
146
|
+
> ok
|
|
147
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
148
148
|
`)
|
|
149
149
|
},
|
|
150
150
|
20_000,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// 2. User queues a message via /queue (enters kimaki local queue)
|
|
8
8
|
// 3. User sends a normal message (interrupt)
|
|
9
9
|
// 4. Session aborts the slow task, processes the interrupt message immediately
|
|
10
|
-
// 5. Interrupt response appears in Discord with a
|
|
10
|
+
// 5. Interrupt response appears in Discord with a ok reply
|
|
11
11
|
// 6. When interrupt response completes, the queued message drains and runs
|
|
12
12
|
//
|
|
13
13
|
// Uses opencode-deterministic-provider (no real LLM calls).
|
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
waitForBotMessageContaining,
|
|
24
24
|
waitForMessageById,
|
|
25
25
|
} from './test-utils.js'
|
|
26
|
+
import { getThreadSession } from './database.js'
|
|
27
|
+
import { archiveThread } from './discord-utils.js'
|
|
28
|
+
import { getOpencodeClient } from './opencode.js'
|
|
26
29
|
|
|
27
30
|
const TEXT_CHANNEL_ID = '200000000000001099'
|
|
28
31
|
|
|
@@ -36,6 +39,124 @@ e2eTest('queue + interrupt drain ordering', () => {
|
|
|
36
39
|
username: 'interrupt-tester',
|
|
37
40
|
})
|
|
38
41
|
|
|
42
|
+
test(
|
|
43
|
+
'archiving keeps the active run alive so queued messages drain',
|
|
44
|
+
async () => {
|
|
45
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
46
|
+
content: 'Reply with exactly: setup-archive-drain',
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
50
|
+
timeout: 4_000,
|
|
51
|
+
predicate: (candidate) => {
|
|
52
|
+
return candidate.name === 'Reply with exactly: setup-archive-drain'
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
const th = ctx.discord.thread(thread.id)
|
|
56
|
+
await th.waitForBotReply({ timeout: 4_000 })
|
|
57
|
+
await waitForFooterMessage({
|
|
58
|
+
discord: ctx.discord,
|
|
59
|
+
threadId: thread.id,
|
|
60
|
+
timeout: 4_000,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
64
|
+
content: 'PLUGIN_TIMEOUT_SLEEP_MARKER archive queue drain',
|
|
65
|
+
})
|
|
66
|
+
await waitForBotMessageContaining({
|
|
67
|
+
discord: ctx.discord,
|
|
68
|
+
threadId: thread.id,
|
|
69
|
+
userId: TEST_USER_ID,
|
|
70
|
+
text: 'starting sleep',
|
|
71
|
+
afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER archive queue drain',
|
|
72
|
+
timeout: 4_000,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const { id: queueInteractionId } = await th.user(TEST_USER_ID)
|
|
76
|
+
.runSlashCommand({
|
|
77
|
+
name: 'queue',
|
|
78
|
+
options: [{
|
|
79
|
+
name: 'message',
|
|
80
|
+
type: 3,
|
|
81
|
+
value: 'Reply with exactly: archived-queue-survives',
|
|
82
|
+
}],
|
|
83
|
+
})
|
|
84
|
+
const queueAck = await th.waitForInteractionAck({
|
|
85
|
+
interactionId: queueInteractionId,
|
|
86
|
+
timeout: 4_000,
|
|
87
|
+
})
|
|
88
|
+
if (!queueAck.messageId) {
|
|
89
|
+
throw new Error('Expected /queue response message id')
|
|
90
|
+
}
|
|
91
|
+
const queueStatusMessage = await waitForMessageById({
|
|
92
|
+
discord: ctx.discord,
|
|
93
|
+
threadId: thread.id,
|
|
94
|
+
messageId: queueAck.messageId,
|
|
95
|
+
timeout: 4_000,
|
|
96
|
+
})
|
|
97
|
+
expect(queueStatusMessage.content).toContain('Queued message')
|
|
98
|
+
|
|
99
|
+
const sessionId = await getThreadSession(thread.id)
|
|
100
|
+
if (!sessionId) throw new Error('Expected mapped OpenCode session')
|
|
101
|
+
const client = getOpencodeClient(ctx.directories.projectDirectory)
|
|
102
|
+
if (!client) throw new Error('Expected initialized OpenCode client')
|
|
103
|
+
await archiveThread({
|
|
104
|
+
rest: ctx.botClient.rest,
|
|
105
|
+
threadId: thread.id,
|
|
106
|
+
parentChannelId: TEXT_CHANNEL_ID,
|
|
107
|
+
sessionId,
|
|
108
|
+
client,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const statusResponse = await client.session.status({
|
|
112
|
+
directory: ctx.directories.projectDirectory,
|
|
113
|
+
})
|
|
114
|
+
expect(statusResponse.data?.[sessionId]?.type).toBe('busy')
|
|
115
|
+
|
|
116
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
117
|
+
content: 'Reply with exactly: continue-after-archive',
|
|
118
|
+
})
|
|
119
|
+
await waitForFooterMessage({
|
|
120
|
+
discord: ctx.discord,
|
|
121
|
+
threadId: thread.id,
|
|
122
|
+
timeout: 12_000,
|
|
123
|
+
afterMessageIncludes: 'continue-after-archive',
|
|
124
|
+
afterAuthorId: TEST_USER_ID,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
await waitForFooterMessage({
|
|
128
|
+
discord: ctx.discord,
|
|
129
|
+
threadId: thread.id,
|
|
130
|
+
timeout: 12_000,
|
|
131
|
+
afterMessageIncludes: 'archived-queue-survives',
|
|
132
|
+
afterAuthorId: ctx.discord.botUserId,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
136
|
+
"--- from: user (interrupt-tester)
|
|
137
|
+
Reply with exactly: setup-archive-drain
|
|
138
|
+
--- from: assistant (TestBot)
|
|
139
|
+
> *using deterministic-provider/deterministic-v2*
|
|
140
|
+
> ok
|
|
141
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
|
|
142
|
+
--- from: user (interrupt-tester)
|
|
143
|
+
PLUGIN_TIMEOUT_SLEEP_MARKER archive queue drain
|
|
144
|
+
--- from: assistant (TestBot)
|
|
145
|
+
> starting sleep 100
|
|
146
|
+
Queued message (position 1)
|
|
147
|
+
--- from: user (interrupt-tester)
|
|
148
|
+
Reply with exactly: continue-after-archive
|
|
149
|
+
--- from: assistant (TestBot)
|
|
150
|
+
> ok
|
|
151
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
|
|
152
|
+
» **interrupt-tester:** Reply with exactly: archived-queue-survives
|
|
153
|
+
> ok
|
|
154
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
155
|
+
`)
|
|
156
|
+
},
|
|
157
|
+
20_000,
|
|
158
|
+
)
|
|
159
|
+
|
|
39
160
|
test(
|
|
40
161
|
'queued message via /queue + normal interrupt: interrupt reply should appear, then queue drains',
|
|
41
162
|
async () => {
|
|
@@ -108,7 +229,7 @@ e2eTest('queue + interrupt drain ordering', () => {
|
|
|
108
229
|
})
|
|
109
230
|
|
|
110
231
|
// 5. Wait for the final state: the interrupt message should get its own
|
|
111
|
-
//
|
|
232
|
+
// ok reply, then the queued message should drain and get processed.
|
|
112
233
|
// We wait for the queued message's footer as the final signal.
|
|
113
234
|
await waitForFooterMessage({
|
|
114
235
|
discord: ctx.discord,
|
|
@@ -123,25 +244,25 @@ e2eTest('queue + interrupt drain ordering', () => {
|
|
|
123
244
|
"--- from: user (interrupt-tester)
|
|
124
245
|
Reply with exactly: setup-interrupt-drain
|
|
125
246
|
--- from: assistant (TestBot)
|
|
126
|
-
*using deterministic-provider/deterministic-v2*
|
|
127
|
-
|
|
128
|
-
*project ⋅ main ⋅
|
|
247
|
+
> *using deterministic-provider/deterministic-v2*
|
|
248
|
+
> ok
|
|
249
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
|
|
129
250
|
--- from: user (interrupt-tester)
|
|
130
251
|
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
131
252
|
--- from: assistant (TestBot)
|
|
132
|
-
|
|
253
|
+
> starting sleep 100
|
|
133
254
|
Queued message (position 1)
|
|
134
255
|
--- from: user (interrupt-tester)
|
|
135
256
|
Reply with exactly: interrupt-now
|
|
136
257
|
--- from: assistant (TestBot)
|
|
137
|
-
|
|
138
|
-
*project ⋅ main ⋅
|
|
258
|
+
> ok
|
|
259
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
|
|
139
260
|
» **interrupt-tester:** Reply with exactly: queued-behind-slow
|
|
140
|
-
|
|
141
|
-
*project ⋅ main ⋅
|
|
261
|
+
> ok
|
|
262
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
142
263
|
`)
|
|
143
264
|
|
|
144
|
-
// 7. Assert the interrupt message got its own
|
|
265
|
+
// 7. Assert the interrupt message got its own ok reply between the
|
|
145
266
|
// user's interrupt message and the queue dispatch indicator.
|
|
146
267
|
const text = await th.text()
|
|
147
268
|
const lines = text.split('\n')
|
|
@@ -158,7 +279,7 @@ e2eTest('queue + interrupt drain ordering', () => {
|
|
|
158
279
|
|
|
159
280
|
const linesBetween = lines.slice(interruptUserLine + 1, queueDispatchLine)
|
|
160
281
|
const hasInterruptReply = linesBetween.some((line) => {
|
|
161
|
-
return line.includes('
|
|
282
|
+
return line.includes('ok')
|
|
162
283
|
})
|
|
163
284
|
expect(hasInterruptReply).toBe(true)
|
|
164
285
|
},
|