kimaki 0.26.0 → 0.27.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 +25 -122
- package/dist/anthropic-account-identity.js +1 -0
- package/dist/anthropic-auth-plugin.js +1 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/send.js +24 -0
- package/dist/cli-commands/session.js +183 -8
- package/dist/cli-commands/task.js +13 -1
- package/dist/cli-parsing.test.js +59 -3
- package/dist/cli-runner.js +28 -4
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +1 -1
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/agent.js +6 -8
- package/dist/commands/ask-question.js +21 -32
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +36 -10
- package/dist/commands/model-variant.js +7 -1
- package/dist/commands/model.js +14 -8
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/database.js +124 -3
- package/dist/db.js +22 -0
- package/dist/db.test.js +167 -1
- package/dist/discord-bot.js +34 -32
- package/dist/discord-command-registration.js +9 -1
- package/dist/discord-utils.js +13 -5
- package/dist/discord-utils.test.js +17 -1
- package/dist/errors.js +5 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +2 -2
- 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 +5 -5
- package/dist/git-worktree-core.js +182 -17
- package/dist/ipc-tools-plugin.js +63 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +7 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +10 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +18 -7
- package/dist/message-formatting.test.js +56 -9
- 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.test.js +60 -0
- package/dist/opencode.js +53 -12
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +1 -1
- package/dist/queue-advanced-action-buttons.e2e.test.js +3 -3
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +40 -246
- package/dist/queue-advanced-model-switch.e2e.test.js +2 -2
- package/dist/queue-advanced-permissions-typing.e2e.test.js +7 -9
- package/dist/queue-advanced-question.e2e.test.js +31 -26
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +2 -2
- package/dist/queue-delete-message.e2e.test.js +1 -1
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +2 -2
- package/dist/queue-interrupt-drain.e2e.test.js +112 -2
- package/dist/queue-question-select-drain.e2e.test.js +30 -128
- package/dist/runtime-lifecycle.e2e.test.js +10 -37
- package/dist/schema.js +34 -0
- package/dist/session-handler/event-stream-state.js +267 -50
- package/dist/session-handler/event-stream-state.test.js +415 -8
- package/dist/session-handler/model-utils.js +159 -5
- package/dist/session-handler/model-utils.test.js +345 -0
- package/dist/session-handler/thread-session-runtime.js +252 -182
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +7 -39
- package/dist/sleep.e2e.test.js +491 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +59 -6
- package/dist/system-message.test.js +76 -6
- package/dist/task-runner.js +94 -4
- package/dist/task-schedule.js +56 -0
- package/dist/task-schedule.test.js +71 -1
- package/dist/thread-message-queue.e2e.test.js +10 -169
- package/dist/undo-redo.e2e.test.js +2 -2
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-message.e2e.test.js +10 -10
- package/dist/voice.test.js +55 -29
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +29 -10
- 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/errore/SKILL.md +18 -0
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +37 -0
- 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 +25 -151
- package/src/anthropic-account-identity.ts +1 -0
- package/src/anthropic-auth-plugin.ts +1 -0
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/send.ts +25 -0
- package/src/cli-commands/session.ts +203 -9
- package/src/cli-commands/task.ts +13 -1
- package/src/cli-parsing.test.ts +69 -3
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +28 -4
- package/src/cli-send-thread.e2e.test.ts +1 -1
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/agent.ts +6 -6
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +21 -32
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +43 -12
- package/src/commands/model-variant.ts +8 -1
- package/src/commands/model.ts +18 -8
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/database.ts +195 -3
- package/src/db.test.ts +188 -0
- package/src/db.ts +26 -0
- package/src/discord-bot.ts +35 -29
- package/src/discord-command-registration.ts +15 -1
- package/src/discord-utils.test.ts +19 -0
- package/src/discord-utils.ts +21 -6
- package/src/errors.ts +5 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +2 -2
- 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 +5 -5
- package/src/git-worktree-core.ts +247 -20
- package/src/ipc-tools-plugin.ts +75 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +7 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +13 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +86 -12
- package/src/message-formatting.ts +19 -7
- 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 +81 -0
- package/src/opencode.ts +71 -11
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +1 -1
- package/src/queue-advanced-action-buttons.e2e.test.ts +3 -3
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +47 -294
- package/src/queue-advanced-model-switch.e2e.test.ts +2 -2
- package/src/queue-advanced-permissions-typing.e2e.test.ts +7 -9
- package/src/queue-advanced-question.e2e.test.ts +35 -29
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +2 -2
- package/src/queue-delete-message.e2e.test.ts +1 -1
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +2 -2
- package/src/queue-interrupt-drain.e2e.test.ts +123 -2
- package/src/queue-question-select-drain.e2e.test.ts +32 -157
- package/src/runtime-lifecycle.e2e.test.ts +10 -41
- package/src/schema.sql +14 -1
- package/src/schema.ts +36 -0
- package/src/session-handler/event-stream-state.test.ts +437 -7
- package/src/session-handler/event-stream-state.ts +389 -53
- package/src/session-handler/model-utils.test.ts +415 -0
- package/src/session-handler/model-utils.ts +249 -5
- package/src/session-handler/thread-session-runtime.ts +300 -215
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +9 -48
- package/src/sleep.e2e.test.ts +562 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +21 -0
- package/src/system-message.test.ts +86 -6
- package/src/system-message.ts +66 -7
- package/src/task-runner.ts +136 -2
- package/src/task-schedule.test.ts +76 -0
- package/src/task-schedule.ts +95 -0
- package/src/thread-message-queue.e2e.test.ts +10 -202
- package/src/undo-redo.e2e.test.ts +2 -2
- package/src/voice-attachment.ts +29 -1
- package/src/voice-message.e2e.test.ts +10 -10
- package/src/voice.test.ts +63 -29
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +29 -9
- 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({
|
|
@@ -108,7 +108,7 @@ e2eTest('queue advanced: typing interrupt', () => {
|
|
|
108
108
|
--- from: assistant (TestBot)
|
|
109
109
|
*using deterministic-provider/deterministic-v2*
|
|
110
110
|
⬥ ok
|
|
111
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
111
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>
|
|
112
112
|
--- from: user (queue-advanced-tester)
|
|
113
113
|
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
114
114
|
--- from: assistant (TestBot)
|
|
@@ -117,7 +117,7 @@ e2eTest('queue advanced: typing interrupt', () => {
|
|
|
117
117
|
Reply with exactly: typing-stop-interrupt-final
|
|
118
118
|
--- from: assistant (TestBot)
|
|
119
119
|
⬥ ok
|
|
120
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
120
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>"
|
|
121
121
|
`)
|
|
122
122
|
|
|
123
123
|
const timeline = await th.text({ showTyping: true })
|
|
@@ -90,7 +90,7 @@ e2eTest('queue delete message', () => {
|
|
|
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
92
|
⬥ slow-busy-reply
|
|
93
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
93
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>"
|
|
94
94
|
`)
|
|
95
95
|
const finalText = await th.text()
|
|
96
96
|
expect(finalText).not.toContain(
|
|
@@ -139,12 +139,12 @@ describe('queue drain with pending interactive UI', () => {
|
|
|
139
139
|
--- from: assistant (TestBot)
|
|
140
140
|
*using deterministic-provider/deterministic-v2*
|
|
141
141
|
⬥ ok
|
|
142
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
142
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>
|
|
143
143
|
**Action Required**
|
|
144
144
|
[user interaction]
|
|
145
145
|
» **drain-ui-tester:** Reply with exactly: post-button-drain
|
|
146
146
|
⬥ ok
|
|
147
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
147
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>"
|
|
148
148
|
`)
|
|
149
149
|
},
|
|
150
150
|
20_000,
|
|
@@ -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 ⋅ Ns ⋅ N% ⋅ 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 ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
152
|
+
» **interrupt-tester:** Reply with exactly: archived-queue-survives
|
|
153
|
+
⬥ ok
|
|
154
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ 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 () => {
|
|
@@ -125,7 +246,7 @@ e2eTest('queue + interrupt drain ordering', () => {
|
|
|
125
246
|
--- from: assistant (TestBot)
|
|
126
247
|
*using deterministic-provider/deterministic-v2*
|
|
127
248
|
⬥ ok
|
|
128
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
249
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>
|
|
129
250
|
--- from: user (interrupt-tester)
|
|
130
251
|
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
131
252
|
--- from: assistant (TestBot)
|
|
@@ -138,7 +259,7 @@ e2eTest('queue + interrupt drain ordering', () => {
|
|
|
138
259
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
139
260
|
» **interrupt-tester:** Reply with exactly: queued-behind-slow
|
|
140
261
|
⬥ ok
|
|
141
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
262
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>"
|
|
142
263
|
`)
|
|
143
264
|
|
|
144
265
|
// 7. Assert the interrupt message got its own ⬥ ok reply between the
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
// E2e test
|
|
2
|
-
// via the Discord dropdown select menu. Reproduces a bug where answering via
|
|
3
|
-
// select (not text) leaves queued messages stuck because the session continues
|
|
4
|
-
// processing after the answer and may enter another blocking state.
|
|
1
|
+
// E2e test for ordered queue drain after a pending question is answered via select.
|
|
5
2
|
|
|
6
3
|
import { describe, test, expect } from 'vitest'
|
|
7
4
|
import {
|
|
@@ -53,10 +50,7 @@ async function expectNoBotMessageContaining({
|
|
|
53
50
|
while (Date.now() - start < timeout) {
|
|
54
51
|
const messages = await discord.thread(threadId).getMessages()
|
|
55
52
|
const match = messages.find((message) => {
|
|
56
|
-
return (
|
|
57
|
-
message.author.id === discord.botUserId
|
|
58
|
-
&& message.content.includes(text)
|
|
59
|
-
)
|
|
53
|
+
return message.author.id === discord.botUserId && message.content.includes(text)
|
|
60
54
|
})
|
|
61
55
|
if (match) {
|
|
62
56
|
throw new Error(
|
|
@@ -78,132 +72,19 @@ describe('queue drain after question select answer', () => {
|
|
|
78
72
|
})
|
|
79
73
|
|
|
80
74
|
test(
|
|
81
|
-
'queued
|
|
75
|
+
'queued messages drain in order after answering via dropdown select',
|
|
82
76
|
async () => {
|
|
83
|
-
|
|
84
|
-
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
85
|
-
content: 'QUESTION_SELECT_QUEUE_MARKER',
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
89
|
-
timeout: 8_000,
|
|
90
|
-
predicate: (t) => {
|
|
91
|
-
return t.name === 'QUESTION_SELECT_QUEUE_MARKER'
|
|
92
|
-
},
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
const th = ctx.discord.thread(thread.id)
|
|
96
|
-
|
|
97
|
-
// 2. Wait for the question dropdown message to appear in Discord.
|
|
98
|
-
// Uses visible message wait instead of internal Map polling which
|
|
99
|
-
// is too timing-sensitive on CI.
|
|
100
|
-
const questionMessages = await waitForBotMessageContaining({
|
|
101
|
-
discord: ctx.discord,
|
|
102
|
-
threadId: thread.id,
|
|
103
|
-
text: 'How to proceed?',
|
|
104
|
-
timeout: 12_000,
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
// Get the pending question context hash from the internal map.
|
|
108
|
-
// By this point the question message is visible so the context must exist.
|
|
109
|
-
const pending = await waitForPendingQuestion({
|
|
110
|
-
threadId: thread.id,
|
|
111
|
-
timeoutMs: 8_000,
|
|
112
|
-
})
|
|
113
|
-
const questionMsg = questionMessages.find((m) => {
|
|
114
|
-
return m.content.includes('How to proceed?')
|
|
115
|
-
})!
|
|
116
|
-
expect(questionMsg).toBeTruthy()
|
|
117
|
-
|
|
118
|
-
// 3. Queue a message while question is pending
|
|
119
|
-
const { id: queueInteractionId } = await th.user(TEST_USER_ID)
|
|
120
|
-
.runSlashCommand({
|
|
121
|
-
name: 'queue',
|
|
122
|
-
options: [{ name: 'message', type: 3, value: 'Reply with exactly: post-question-drain' }],
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
const queueAck = await th.waitForInteractionAck({
|
|
126
|
-
interactionId: queueInteractionId,
|
|
127
|
-
timeout: 8_000,
|
|
128
|
-
})
|
|
129
|
-
if (!queueAck.messageId) {
|
|
130
|
-
throw new Error('Expected /queue response message id')
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 4. The first queued item should be handed off immediately even while
|
|
134
|
-
// the question is still pending, so the visible dispatch indicator
|
|
135
|
-
// appears before the user answers the dropdown.
|
|
136
|
-
await waitForBotMessageContaining({
|
|
137
|
-
discord: ctx.discord,
|
|
138
|
-
threadId: thread.id,
|
|
139
|
-
text: '» **question-select-tester:** Reply with exactly: post-question-drain',
|
|
140
|
-
timeout: 8_000,
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
// 5. Answer the question via dropdown select (pick first option "Alpha")
|
|
144
|
-
const interaction = await th.user(TEST_USER_ID).selectMenu({
|
|
145
|
-
messageId: questionMsg.id,
|
|
146
|
-
customId: `ask_question:${pending.contextHash}:0`,
|
|
147
|
-
values: ['0'],
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
await th.waitForInteractionAck({
|
|
151
|
-
interactionId: interaction.id,
|
|
152
|
-
timeout: 8_000,
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
// 6. Wait for footer from the drained queued message
|
|
156
|
-
await waitForFooterMessage({
|
|
157
|
-
discord: ctx.discord,
|
|
158
|
-
threadId: thread.id,
|
|
159
|
-
timeout: 8_000,
|
|
160
|
-
afterMessageIncludes: '» **question-select-tester:**',
|
|
161
|
-
afterAuthorId: ctx.discord.botUserId,
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
const timeline = await th.text({ showInteractions: true })
|
|
165
|
-
expect(timeline).toMatchInlineSnapshot(`
|
|
166
|
-
"--- from: user (question-select-tester)
|
|
167
|
-
QUESTION_SELECT_QUEUE_MARKER
|
|
168
|
-
--- from: assistant (TestBot)
|
|
169
|
-
*using deterministic-provider/deterministic-v2*
|
|
170
|
-
**Select action**
|
|
171
|
-
How to proceed?
|
|
172
|
-
✓ _Alpha_
|
|
173
|
-
[user interaction]
|
|
174
|
-
» **question-select-tester:** Reply with exactly: post-question-drain
|
|
175
|
-
Queued message (position 1)
|
|
176
|
-
[user selects dropdown: 0]
|
|
177
|
-
» **question-select-tester:** Alpha
|
|
178
|
-
⬥ ok
|
|
179
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
180
|
-
`)
|
|
181
|
-
expect(timeline).toContain('QUESTION_SELECT_QUEUE_MARKER')
|
|
182
|
-
expect(timeline).toContain('How to proceed?')
|
|
183
|
-
expect(timeline).toContain('[user selects dropdown: 0]')
|
|
184
|
-
expect(timeline).toContain('» **question-select-tester:** Reply with exactly: post-question-drain')
|
|
185
|
-
expect(timeline).toContain('⬥ ok')
|
|
186
|
-
expect(timeline).toContain('*project ⋅ main ⋅')
|
|
187
|
-
},
|
|
188
|
-
20_000,
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
test(
|
|
192
|
-
'only the first queued message is handed off after dropdown answer',
|
|
193
|
-
async () => {
|
|
194
|
-
const marker = 'QUESTION_SELECT_QUEUE_MARKER second-test'
|
|
195
|
-
|
|
77
|
+
const marker = 'QUESTION_SELECT_QUEUE_MARKER'
|
|
196
78
|
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
197
79
|
content: marker,
|
|
198
80
|
})
|
|
199
81
|
|
|
200
82
|
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
201
83
|
timeout: 8_000,
|
|
202
|
-
predicate: (
|
|
203
|
-
return
|
|
84
|
+
predicate: (candidate) => {
|
|
85
|
+
return candidate.name === marker
|
|
204
86
|
},
|
|
205
87
|
})
|
|
206
|
-
|
|
207
88
|
const th = ctx.discord.thread(thread.id)
|
|
208
89
|
|
|
209
90
|
const questionMessages = await waitForBotMessageContaining({
|
|
@@ -212,21 +93,18 @@ describe('queue drain after question select answer', () => {
|
|
|
212
93
|
text: 'How to proceed?',
|
|
213
94
|
timeout: 12_000,
|
|
214
95
|
})
|
|
215
|
-
|
|
216
96
|
const pending = await waitForPendingQuestion({
|
|
217
97
|
threadId: thread.id,
|
|
218
98
|
timeoutMs: 8_000,
|
|
219
99
|
})
|
|
220
|
-
|
|
221
|
-
const questionMsg = questionMessages.find((message) => {
|
|
100
|
+
const questionMessage = questionMessages.find((message) => {
|
|
222
101
|
return message.content.includes('How to proceed?')
|
|
223
102
|
})
|
|
224
|
-
|
|
225
|
-
if (!questionMsg) {
|
|
103
|
+
if (!questionMessage) {
|
|
226
104
|
throw new Error('Expected question message')
|
|
227
105
|
}
|
|
228
106
|
|
|
229
|
-
const firstQueuedPrompt = '
|
|
107
|
+
const firstQueuedPrompt = 'QUESTION_SELECT_DRAIN_FIRST_MARKER'
|
|
230
108
|
const secondQueuedPrompt = 'Reply with exactly: post-question-second'
|
|
231
109
|
|
|
232
110
|
const { id: firstQueueInteractionId } = await th.user(TEST_USER_ID)
|
|
@@ -234,48 +112,45 @@ describe('queue drain after question select answer', () => {
|
|
|
234
112
|
name: 'queue',
|
|
235
113
|
options: [{ name: 'message', type: 3, value: firstQueuedPrompt }],
|
|
236
114
|
})
|
|
237
|
-
|
|
238
115
|
await th.waitForInteractionAck({
|
|
239
116
|
interactionId: firstQueueInteractionId,
|
|
240
117
|
timeout: 8_000,
|
|
241
118
|
})
|
|
242
119
|
|
|
120
|
+
await waitForBotMessageContaining({
|
|
121
|
+
discord: ctx.discord,
|
|
122
|
+
threadId: thread.id,
|
|
123
|
+
text: `» **question-select-tester:** ${firstQueuedPrompt}`,
|
|
124
|
+
timeout: 8_000,
|
|
125
|
+
})
|
|
126
|
+
|
|
243
127
|
const { id: secondQueueInteractionId } = await th.user(TEST_USER_ID)
|
|
244
128
|
.runSlashCommand({
|
|
245
129
|
name: 'queue',
|
|
246
130
|
options: [{ name: 'message', type: 3, value: secondQueuedPrompt }],
|
|
247
131
|
})
|
|
248
|
-
|
|
249
132
|
await th.waitForInteractionAck({
|
|
250
133
|
interactionId: secondQueueInteractionId,
|
|
251
134
|
timeout: 8_000,
|
|
252
135
|
})
|
|
253
136
|
|
|
137
|
+
await expectNoBotMessageContaining({
|
|
138
|
+
discord: ctx.discord,
|
|
139
|
+
threadId: thread.id,
|
|
140
|
+
text: `» **question-select-tester:** ${secondQueuedPrompt}`,
|
|
141
|
+
timeout: 200,
|
|
142
|
+
})
|
|
143
|
+
|
|
254
144
|
const interaction = await th.user(TEST_USER_ID).selectMenu({
|
|
255
|
-
messageId:
|
|
145
|
+
messageId: questionMessage.id,
|
|
256
146
|
customId: `ask_question:${pending.contextHash}:0`,
|
|
257
147
|
values: ['0'],
|
|
258
148
|
})
|
|
259
|
-
|
|
260
149
|
await th.waitForInteractionAck({
|
|
261
150
|
interactionId: interaction.id,
|
|
262
151
|
timeout: 8_000,
|
|
263
152
|
})
|
|
264
153
|
|
|
265
|
-
await waitForBotMessageContaining({
|
|
266
|
-
discord: ctx.discord,
|
|
267
|
-
threadId: thread.id,
|
|
268
|
-
text: `» **question-select-tester:** ${firstQueuedPrompt}`,
|
|
269
|
-
timeout: 8_000,
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
await expectNoBotMessageContaining({
|
|
273
|
-
discord: ctx.discord,
|
|
274
|
-
threadId: thread.id,
|
|
275
|
-
text: `» **question-select-tester:** ${secondQueuedPrompt}`,
|
|
276
|
-
timeout: 200,
|
|
277
|
-
})
|
|
278
|
-
|
|
279
154
|
await waitForFooterMessage({
|
|
280
155
|
discord: ctx.discord,
|
|
281
156
|
threadId: thread.id,
|
|
@@ -283,14 +158,12 @@ describe('queue drain after question select answer', () => {
|
|
|
283
158
|
afterMessageIncludes: `» **question-select-tester:** ${firstQueuedPrompt}`,
|
|
284
159
|
afterAuthorId: ctx.discord.botUserId,
|
|
285
160
|
})
|
|
286
|
-
|
|
287
161
|
await waitForBotMessageContaining({
|
|
288
162
|
discord: ctx.discord,
|
|
289
163
|
threadId: thread.id,
|
|
290
164
|
text: `» **question-select-tester:** ${secondQueuedPrompt}`,
|
|
291
165
|
timeout: 8_000,
|
|
292
166
|
})
|
|
293
|
-
|
|
294
167
|
await waitForFooterMessage({
|
|
295
168
|
discord: ctx.discord,
|
|
296
169
|
threadId: thread.id,
|
|
@@ -302,30 +175,32 @@ describe('queue drain after question select answer', () => {
|
|
|
302
175
|
const timeline = await th.text({ showInteractions: true })
|
|
303
176
|
expect(timeline).toMatchInlineSnapshot(`
|
|
304
177
|
"--- from: user (question-select-tester)
|
|
305
|
-
QUESTION_SELECT_QUEUE_MARKER
|
|
178
|
+
QUESTION_SELECT_QUEUE_MARKER
|
|
306
179
|
--- from: assistant (TestBot)
|
|
307
180
|
*using deterministic-provider/deterministic-v2*
|
|
308
181
|
**Select action**
|
|
309
182
|
How to proceed?
|
|
310
183
|
✓ _Alpha_
|
|
311
184
|
[user interaction]
|
|
312
|
-
» **question-select-tester:**
|
|
185
|
+
» **question-select-tester:** QUESTION_SELECT_DRAIN_FIRST_MARKER
|
|
313
186
|
Queued message (position 1)
|
|
314
187
|
[user interaction]
|
|
315
188
|
Queued message (position 1)
|
|
316
189
|
[user selects dropdown: 0]
|
|
317
190
|
» **question-select-tester:** Alpha
|
|
318
|
-
⬥
|
|
191
|
+
⬥ question-drain-first
|
|
319
192
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
320
193
|
» **question-select-tester:** Reply with exactly: post-question-second
|
|
321
194
|
⬥ ok
|
|
322
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
195
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>"
|
|
323
196
|
`)
|
|
197
|
+
expect(timeline).toContain('How to proceed?')
|
|
198
|
+
expect(timeline).toContain('[user selects dropdown: 0]')
|
|
324
199
|
expect(timeline).toContain(`» **question-select-tester:** ${firstQueuedPrompt}`)
|
|
325
|
-
expect(timeline).toContain('⬥
|
|
200
|
+
expect(timeline).toContain('⬥ question-drain-first')
|
|
326
201
|
expect(timeline).toContain(`» **question-select-tester:** ${secondQueuedPrompt}`)
|
|
327
202
|
expect(timeline).toContain('⬥ ok')
|
|
328
203
|
},
|
|
329
|
-
|
|
204
|
+
15_000,
|
|
330
205
|
)
|
|
331
206
|
})
|