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
|
@@ -124,13 +124,13 @@ describe('queue advanced: action buttons', () => {
|
|
|
124
124
|
--- from: assistant (TestBot)
|
|
125
125
|
*using deterministic-provider/deterministic-v2*
|
|
126
126
|
⬥ ok
|
|
127
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
127
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>
|
|
128
128
|
**Action Required**
|
|
129
129
|
_Selected: Continue action-buttons flow_
|
|
130
130
|
[user clicks button]
|
|
131
131
|
» **queue-action-tester:** Continue action-buttons flow
|
|
132
132
|
⬥ action-buttons-click-continued
|
|
133
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
133
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>"
|
|
134
134
|
`);
|
|
135
135
|
expect(timeline).toContain('action-buttons-click-continued');
|
|
136
136
|
}, 20_000);
|
|
@@ -197,7 +197,7 @@ describe('queue advanced: action buttons', () => {
|
|
|
197
197
|
--- from: assistant (TestBot)
|
|
198
198
|
*using deterministic-provider/deterministic-v2*
|
|
199
199
|
⬥ ok
|
|
200
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
200
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>
|
|
201
201
|
**Action Required**
|
|
202
202
|
_Buttons dismissed._
|
|
203
203
|
--- from: user (queue-action-tester)
|
|
@@ -78,6 +78,31 @@ export function createDeterministicMatchers() {
|
|
|
78
78
|
partDelaysMs: [0, 500, 0, 0, 0],
|
|
79
79
|
},
|
|
80
80
|
};
|
|
81
|
+
const questionSelectDrainMatcher = {
|
|
82
|
+
id: 'question-select-drain-first',
|
|
83
|
+
priority: 109,
|
|
84
|
+
when: {
|
|
85
|
+
latestUserTextIncludes: 'QUESTION_SELECT_DRAIN_FIRST_MARKER',
|
|
86
|
+
},
|
|
87
|
+
then: {
|
|
88
|
+
parts: [
|
|
89
|
+
{ type: 'stream-start', warnings: [] },
|
|
90
|
+
{ type: 'text-start', id: 'question-select-drain-first' },
|
|
91
|
+
{
|
|
92
|
+
type: 'text-delta',
|
|
93
|
+
id: 'question-select-drain-first',
|
|
94
|
+
delta: 'question-drain-first',
|
|
95
|
+
},
|
|
96
|
+
{ type: 'text-end', id: 'question-select-drain-first' },
|
|
97
|
+
{
|
|
98
|
+
type: 'finish',
|
|
99
|
+
finishReason: 'stop',
|
|
100
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
partDelaysMs: [0, 0, 0, 400, 0],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
81
106
|
const slowAbortMatcher = {
|
|
82
107
|
id: 'slow-abort-marker',
|
|
83
108
|
priority: 100,
|
|
@@ -260,9 +285,7 @@ export function createDeterministicMatchers() {
|
|
|
260
285
|
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
261
286
|
},
|
|
262
287
|
],
|
|
263
|
-
|
|
264
|
-
// must pulse again. This makes typing resume assertions deterministic.
|
|
265
|
-
partDelaysMs: [0, 0, 0, 0, 8_000],
|
|
288
|
+
partDelaysMs: [0, 400, 0, 0, 0],
|
|
266
289
|
},
|
|
267
290
|
};
|
|
268
291
|
const externalDirectoryProbeMatcher = {
|
|
@@ -382,6 +405,7 @@ export function createDeterministicMatchers() {
|
|
|
382
405
|
when: {
|
|
383
406
|
lastMessageRole: 'user',
|
|
384
407
|
latestUserTextIncludes: 'QUESTION_TEXT_ANSWER_MARKER',
|
|
408
|
+
rawPromptRegex: '^(?!.*question-text-answer-call)',
|
|
385
409
|
},
|
|
386
410
|
then: {
|
|
387
411
|
parts: [
|
|
@@ -417,6 +441,7 @@ export function createDeterministicMatchers() {
|
|
|
417
441
|
when: {
|
|
418
442
|
lastMessageRole: 'user',
|
|
419
443
|
latestUserTextIncludes: 'QUESTION_SELECT_QUEUE_MARKER',
|
|
444
|
+
rawPromptRegex: '^(?!.*question-select-queue-call)',
|
|
420
445
|
},
|
|
421
446
|
then: {
|
|
422
447
|
parts: [
|
|
@@ -444,6 +469,49 @@ export function createDeterministicMatchers() {
|
|
|
444
469
|
],
|
|
445
470
|
},
|
|
446
471
|
};
|
|
472
|
+
// Text then question, with text-end delayed after the tool call.
|
|
473
|
+
// Reproduces OpenCode emitting question.asked before the text part's time.end.
|
|
474
|
+
const questionAfterTextMatcher = {
|
|
475
|
+
id: 'question-after-text-marker',
|
|
476
|
+
priority: 108,
|
|
477
|
+
when: {
|
|
478
|
+
lastMessageRole: 'user',
|
|
479
|
+
latestUserTextIncludes: 'QUESTION_AFTER_TEXT_MARKER',
|
|
480
|
+
},
|
|
481
|
+
then: {
|
|
482
|
+
parts: [
|
|
483
|
+
{ type: 'stream-start', warnings: [] },
|
|
484
|
+
{ type: 'text-start', id: 'plan-before-question' },
|
|
485
|
+
{
|
|
486
|
+
type: 'text-delta',
|
|
487
|
+
id: 'plan-before-question',
|
|
488
|
+
delta: 'PLAN_TEXT_BEFORE_QUESTION',
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
type: 'tool-call',
|
|
492
|
+
toolCallId: 'question-after-text-call',
|
|
493
|
+
toolName: 'question',
|
|
494
|
+
input: JSON.stringify({
|
|
495
|
+
questions: [{
|
|
496
|
+
question: 'What next?',
|
|
497
|
+
header: 'Next step',
|
|
498
|
+
options: [
|
|
499
|
+
{ label: 'Commit', description: 'Commit these files' },
|
|
500
|
+
{ label: 'Stop', description: 'Leave uncommitted' },
|
|
501
|
+
],
|
|
502
|
+
}],
|
|
503
|
+
}),
|
|
504
|
+
},
|
|
505
|
+
{ type: 'text-end', id: 'plan-before-question' },
|
|
506
|
+
{
|
|
507
|
+
type: 'finish',
|
|
508
|
+
finishReason: 'tool-calls',
|
|
509
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
partDelaysMs: [200, 0, 0, 0, 400, 0],
|
|
513
|
+
},
|
|
514
|
+
};
|
|
447
515
|
// Model responds with text + tool call, then after tool result the
|
|
448
516
|
// follow-up matcher responds with text. This creates two assistant messages:
|
|
449
517
|
// first with finish="tool-calls" + completed, second with finish="stop".
|
|
@@ -753,6 +821,7 @@ export function createDeterministicMatchers() {
|
|
|
753
821
|
},
|
|
754
822
|
};
|
|
755
823
|
return [
|
|
824
|
+
questionSelectDrainMatcher,
|
|
756
825
|
slowAbortMatcher,
|
|
757
826
|
slowBusyMatcher,
|
|
758
827
|
typingRepulseMatcher,
|
|
@@ -760,6 +829,7 @@ export function createDeterministicMatchers() {
|
|
|
760
829
|
actionButtonClickFollowupMatcher,
|
|
761
830
|
questionToolMatcher,
|
|
762
831
|
questionSelectQueueMatcher,
|
|
832
|
+
questionAfterTextMatcher,
|
|
763
833
|
permissionTypingMatcher,
|
|
764
834
|
permissionTypingFollowupMatcher,
|
|
765
835
|
externalDirectoryProbeMatcher,
|
|
@@ -787,7 +857,7 @@ export const TEST_USER_ID = '200000000000000991';
|
|
|
787
857
|
* Each caller should use a unique channelId and dirName to avoid collisions
|
|
788
858
|
* when vitest runs files in parallel.
|
|
789
859
|
*/
|
|
790
|
-
export function setupQueueAdvancedSuite({ channelId, channelName, dirName, username, restrictExternalDirectories = false, projectPermission, }) {
|
|
860
|
+
export function setupQueueAdvancedSuite({ channelId, channelName, dirName, username, restrictExternalDirectories = false, projectPermission, extraMatchers = [], }) {
|
|
791
861
|
const ctx = {
|
|
792
862
|
directories: undefined,
|
|
793
863
|
discord: undefined,
|
|
@@ -828,7 +898,10 @@ export function setupQueueAdvancedSuite({ channelId, channelName, dirName, usern
|
|
|
828
898
|
providerNpm,
|
|
829
899
|
model: 'deterministic-v2',
|
|
830
900
|
smallModel: 'deterministic-v3',
|
|
831
|
-
settings: {
|
|
901
|
+
settings: {
|
|
902
|
+
strict: false,
|
|
903
|
+
matchers: [...createDeterministicMatchers(), ...extraMatchers],
|
|
904
|
+
},
|
|
832
905
|
});
|
|
833
906
|
fs.writeFileSync(path.join(ctx.directories.projectDirectory, 'opencode.json'), JSON.stringify(projectPermission
|
|
834
907
|
? { ...opencodeConfig, permission: projectPermission }
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Split from thread-queue-advanced.e2e.test.ts for parallelization.
|
|
3
3
|
import { describe, test, expect } from 'vitest';
|
|
4
4
|
import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
|
|
5
|
+
import { store } from './store.js';
|
|
5
6
|
import { waitForFooterMessage, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, } from './test-utils.js';
|
|
6
7
|
const TEXT_CHANNEL_ID = '200000000000001001';
|
|
7
8
|
const e2eTest = describe;
|
|
@@ -35,7 +36,7 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
35
36
|
--- from: assistant (TestBot)
|
|
36
37
|
*using deterministic-provider/deterministic-v2*
|
|
37
38
|
⬥ ok
|
|
38
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
39
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>"
|
|
39
40
|
`);
|
|
40
41
|
const foundFooter = footerMessages.some((m) => {
|
|
41
42
|
return m.author.id === ctx.discord.botUserId
|
|
@@ -44,6 +45,38 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
44
45
|
});
|
|
45
46
|
expect(foundFooter).toBe(true);
|
|
46
47
|
}, 8_000);
|
|
48
|
+
test('skip-footer-mentions omits the thread creator mention', async () => {
|
|
49
|
+
store.setState({ footerMentionsEnabled: false });
|
|
50
|
+
try {
|
|
51
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
52
|
+
content: 'Reply with exactly: footer-without-mention',
|
|
53
|
+
});
|
|
54
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
55
|
+
timeout: 4_000,
|
|
56
|
+
predicate: (t) => {
|
|
57
|
+
return t.name === 'Reply with exactly: footer-without-mention';
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const th = ctx.discord.thread(thread.id);
|
|
61
|
+
await th.waitForBotReply({ timeout: 4_000 });
|
|
62
|
+
await waitForFooterMessage({
|
|
63
|
+
discord: ctx.discord,
|
|
64
|
+
threadId: thread.id,
|
|
65
|
+
timeout: 4_000,
|
|
66
|
+
});
|
|
67
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
68
|
+
"--- from: user (queue-advanced-tester)
|
|
69
|
+
Reply with exactly: footer-without-mention
|
|
70
|
+
--- from: assistant (TestBot)
|
|
71
|
+
*using deterministic-provider/deterministic-v2*
|
|
72
|
+
⬥ ok
|
|
73
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
store.setState({ footerMentionsEnabled: true });
|
|
78
|
+
}
|
|
79
|
+
}, 8_000);
|
|
47
80
|
test('footer appears after second message in same session', async () => {
|
|
48
81
|
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
49
82
|
content: 'Reply with exactly: footer-multi-setup',
|
|
@@ -92,12 +125,12 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
92
125
|
--- from: assistant (TestBot)
|
|
93
126
|
*using deterministic-provider/deterministic-v2*
|
|
94
127
|
⬥ ok
|
|
95
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
128
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>
|
|
96
129
|
--- from: user (queue-advanced-tester)
|
|
97
130
|
Reply with exactly: footer-multi-second
|
|
98
131
|
--- from: assistant (TestBot)
|
|
99
132
|
⬥ ok
|
|
100
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
133
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>"
|
|
101
134
|
`);
|
|
102
135
|
if (footerCount >= 2) {
|
|
103
136
|
expect(footerCount).toBeGreaterThanOrEqual(2);
|
|
@@ -189,7 +222,7 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
189
222
|
--- from: assistant (TestBot)
|
|
190
223
|
*using deterministic-provider/deterministic-v2*
|
|
191
224
|
⬥ ok
|
|
192
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
225
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>
|
|
193
226
|
--- from: user (queue-advanced-tester)
|
|
194
227
|
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
195
228
|
--- from: assistant (TestBot)
|
|
@@ -198,7 +231,7 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
198
231
|
Reply with exactly: interrupt-footer-followup
|
|
199
232
|
--- from: assistant (TestBot)
|
|
200
233
|
⬥ ok
|
|
201
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
234
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>"
|
|
202
235
|
`);
|
|
203
236
|
expect(followupUserIdx).toBeGreaterThanOrEqual(0);
|
|
204
237
|
expect(okReplyIdx).toBeGreaterThan(followupUserIdx);
|
|
@@ -212,102 +245,7 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
212
245
|
});
|
|
213
246
|
expect(footerBetween).toBe(false);
|
|
214
247
|
}, 15_000);
|
|
215
|
-
test('
|
|
216
|
-
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
217
|
-
content: 'Reply with exactly: plugin-timeout-setup',
|
|
218
|
-
});
|
|
219
|
-
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
220
|
-
timeout: 4_000,
|
|
221
|
-
predicate: (t) => {
|
|
222
|
-
return t.name === 'Reply with exactly: plugin-timeout-setup';
|
|
223
|
-
},
|
|
224
|
-
});
|
|
225
|
-
const th = ctx.discord.thread(thread.id);
|
|
226
|
-
await th.waitForBotReply({ timeout: 4_000 });
|
|
227
|
-
await waitForBotMessageContaining({
|
|
228
|
-
discord: ctx.discord,
|
|
229
|
-
threadId: thread.id,
|
|
230
|
-
userId: TEST_USER_ID,
|
|
231
|
-
text: '*project',
|
|
232
|
-
timeout: 4_000,
|
|
233
|
-
});
|
|
234
|
-
await th.user(TEST_USER_ID).sendMessage({
|
|
235
|
-
content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
|
|
236
|
-
});
|
|
237
|
-
await waitForBotMessageContaining({
|
|
238
|
-
discord: ctx.discord,
|
|
239
|
-
threadId: thread.id,
|
|
240
|
-
userId: TEST_USER_ID,
|
|
241
|
-
text: 'starting sleep 100',
|
|
242
|
-
afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
|
|
243
|
-
timeout: 4_000,
|
|
244
|
-
});
|
|
245
|
-
await th.user(TEST_USER_ID).sendMessage({
|
|
246
|
-
content: 'Reply with exactly: plugin-timeout-after',
|
|
247
|
-
});
|
|
248
|
-
const messages = await waitForBotMessageContaining({
|
|
249
|
-
discord: ctx.discord,
|
|
250
|
-
threadId: thread.id,
|
|
251
|
-
userId: TEST_USER_ID,
|
|
252
|
-
text: 'ok',
|
|
253
|
-
afterUserMessageIncludes: 'plugin-timeout-after',
|
|
254
|
-
timeout: 12_000,
|
|
255
|
-
});
|
|
256
|
-
const messagesWithFooter = await waitForFooterMessage({
|
|
257
|
-
discord: ctx.discord,
|
|
258
|
-
threadId: thread.id,
|
|
259
|
-
timeout: 12_000,
|
|
260
|
-
afterMessageIncludes: 'plugin-timeout-after',
|
|
261
|
-
afterAuthorId: TEST_USER_ID,
|
|
262
|
-
});
|
|
263
|
-
const afterIndex = messagesWithFooter.findIndex((message) => {
|
|
264
|
-
return (message.author.id === TEST_USER_ID
|
|
265
|
-
&& message.content.includes('plugin-timeout-after'));
|
|
266
|
-
});
|
|
267
|
-
expect(await th.text()).toMatchInlineSnapshot(`
|
|
268
|
-
"--- from: user (queue-advanced-tester)
|
|
269
|
-
Reply with exactly: plugin-timeout-setup
|
|
270
|
-
--- from: assistant (TestBot)
|
|
271
|
-
*using deterministic-provider/deterministic-v2*
|
|
272
|
-
⬥ ok
|
|
273
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
274
|
-
--- from: user (queue-advanced-tester)
|
|
275
|
-
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
276
|
-
--- from: assistant (TestBot)
|
|
277
|
-
⬥ starting sleep 100
|
|
278
|
-
--- from: user (queue-advanced-tester)
|
|
279
|
-
Reply with exactly: plugin-timeout-after
|
|
280
|
-
--- from: assistant (TestBot)
|
|
281
|
-
⬥ ok
|
|
282
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
283
|
-
`);
|
|
284
|
-
expect(afterIndex).toBeGreaterThanOrEqual(0);
|
|
285
|
-
const okReplyIndex = messagesWithFooter.findIndex((message, index) => {
|
|
286
|
-
if (index <= afterIndex) {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
return message.author.id === ctx.discord.botUserId && message.content.includes('ok');
|
|
290
|
-
});
|
|
291
|
-
expect(okReplyIndex).toBeGreaterThan(afterIndex);
|
|
292
|
-
const footerBeforeReply = messagesWithFooter.some((message, index) => {
|
|
293
|
-
if (index <= afterIndex || index >= okReplyIndex) {
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
if (message.author.id !== ctx.discord.botUserId) {
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
return message.content.startsWith('*') && message.content.includes('⋅');
|
|
300
|
-
});
|
|
301
|
-
expect(footerBeforeReply).toBe(false);
|
|
302
|
-
}, 15_000);
|
|
303
|
-
test('tool-call assistant message gets footer when it completes normally', async () => {
|
|
304
|
-
// Reproduces the bug: model responds with text + tool call,
|
|
305
|
-
// finish="tool-calls", message gets completed timestamp. Then the tool
|
|
306
|
-
// result triggers a follow-up text response in a second assistant message.
|
|
307
|
-
// The second message gets a footer, but the first (tool-call) message
|
|
308
|
-
// should ALSO get a footer since it completed normally.
|
|
309
|
-
// This matches the real-world scenario where an agent calls a bash tool
|
|
310
|
-
// (e.g. `kimaki send`) and then follows up with a summary text.
|
|
248
|
+
test('tool-call turn emits a footer after the final follow-up', async () => {
|
|
311
249
|
const existingThreadIds = new Set((await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
|
|
312
250
|
return thread.id;
|
|
313
251
|
}));
|
|
@@ -337,26 +275,6 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
337
275
|
threadId: thread.id,
|
|
338
276
|
timeout: 4_000,
|
|
339
277
|
});
|
|
340
|
-
// Poll until both footers have arrived — the first footer (after the
|
|
341
|
-
// tool-call step) and the second (after the text follow-up) are emitted
|
|
342
|
-
// by sequential handleNaturalAssistantCompletion calls but the second
|
|
343
|
-
// may not have hit the Discord thread by the time we first check.
|
|
344
|
-
const deadline = Date.now() + 4_000;
|
|
345
|
-
let footerCount = 0;
|
|
346
|
-
while (Date.now() < deadline) {
|
|
347
|
-
const msgs = await th.getMessages();
|
|
348
|
-
footerCount = msgs.filter((m) => {
|
|
349
|
-
return m.author.id === ctx.discord.botUserId
|
|
350
|
-
&& m.content.startsWith('*')
|
|
351
|
-
&& m.content.includes('⋅');
|
|
352
|
-
}).length;
|
|
353
|
-
if (footerCount >= 2) {
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
await new Promise((resolve) => {
|
|
357
|
-
setTimeout(resolve, 100);
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
278
|
expect(await th.text()).toMatchInlineSnapshot(`
|
|
361
279
|
"--- from: user (queue-advanced-tester)
|
|
362
280
|
TOOL_CALL_FOOTER_MARKER
|
|
@@ -365,131 +283,7 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
365
283
|
⬥ running tool
|
|
366
284
|
┣ bash _echo tool-call-footer-test_
|
|
367
285
|
⬥ tool call completed
|
|
368
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
286
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>"
|
|
369
287
|
`);
|
|
370
|
-
// Only ONE footer at the end — the tool-call step's footer is NOT
|
|
371
|
-
// emitted mid-turn. The final text follow-up gets the footer.
|
|
372
|
-
expect(footerCount).toBe(1);
|
|
373
288
|
}, 10_000);
|
|
374
|
-
test('multi-step tool chain should only have one footer at the end', async () => {
|
|
375
|
-
// Model does 3 sequential tool calls (each a separate assistant message
|
|
376
|
-
// with finish="tool-calls") then a final text response. Only the final
|
|
377
|
-
// text response should get a footer — intermediate tool-call steps
|
|
378
|
-
// should NOT get footers since they're mid-turn work.
|
|
379
|
-
const existingThreadIds = new Set((await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
|
|
380
|
-
return thread.id;
|
|
381
|
-
}));
|
|
382
|
-
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
383
|
-
content: 'MULTI_TOOL_FOOTER_MARKER',
|
|
384
|
-
});
|
|
385
|
-
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
386
|
-
timeout: 6_000,
|
|
387
|
-
predicate: (t) => {
|
|
388
|
-
return !existingThreadIds.has(t.id);
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
|
-
const th = ctx.discord.thread(thread.id);
|
|
392
|
-
// Wait for the final text response after all 3 tool steps
|
|
393
|
-
await waitForBotMessageContaining({
|
|
394
|
-
discord: ctx.discord,
|
|
395
|
-
threadId: thread.id,
|
|
396
|
-
userId: TEST_USER_ID,
|
|
397
|
-
text: 'all done, fixed 3 files',
|
|
398
|
-
timeout: 6_000,
|
|
399
|
-
});
|
|
400
|
-
// Wait for the footer after the final response
|
|
401
|
-
await waitForFooterMessage({
|
|
402
|
-
discord: ctx.discord,
|
|
403
|
-
threadId: thread.id,
|
|
404
|
-
timeout: 6_000,
|
|
405
|
-
});
|
|
406
|
-
// Give any spurious extra footers time to arrive
|
|
407
|
-
await new Promise((resolve) => {
|
|
408
|
-
setTimeout(resolve, 500);
|
|
409
|
-
});
|
|
410
|
-
const messages = await th.getMessages();
|
|
411
|
-
const footerCount = messages.filter((m) => {
|
|
412
|
-
return m.author.id === ctx.discord.botUserId
|
|
413
|
-
&& m.content.startsWith('*')
|
|
414
|
-
&& m.content.includes('⋅');
|
|
415
|
-
}).length;
|
|
416
|
-
expect(await th.text()).toMatchInlineSnapshot(`
|
|
417
|
-
"--- from: user (queue-advanced-tester)
|
|
418
|
-
MULTI_TOOL_FOOTER_MARKER
|
|
419
|
-
--- from: assistant (TestBot)
|
|
420
|
-
*using deterministic-provider/deterministic-v2*
|
|
421
|
-
⬥ investigating the issue
|
|
422
|
-
┣ bash _echo search-done_
|
|
423
|
-
┣ bash _echo read-done_
|
|
424
|
-
┣ bash _echo fix-done_
|
|
425
|
-
⬥ all done, fixed 3 files
|
|
426
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
427
|
-
`);
|
|
428
|
-
// Only ONE footer should appear — after the final text response.
|
|
429
|
-
// Intermediate tool-call steps should NOT get footers.
|
|
430
|
-
expect(footerCount).toBe(1);
|
|
431
|
-
}, 10_000);
|
|
432
|
-
test('3 sequential tool-call steps produce exactly 1 footer, not 3', async () => {
|
|
433
|
-
// This is the most obvious reproduction of the multi-footer bug:
|
|
434
|
-
// the model runs 3 sequential tool-call steps (each a SEPARATE
|
|
435
|
-
// assistant message with finish="tool-calls"), then a final text.
|
|
436
|
-
// With a naive fix that treats tool-calls as natural completions,
|
|
437
|
-
// you'd see 4 footers (one per assistant message). Only the final
|
|
438
|
-
// text response should produce a footer.
|
|
439
|
-
const existingThreadIds = new Set((await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
|
|
440
|
-
return thread.id;
|
|
441
|
-
}));
|
|
442
|
-
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
443
|
-
content: 'MULTI_STEP_CHAIN_MARKER',
|
|
444
|
-
});
|
|
445
|
-
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
446
|
-
timeout: 6_000,
|
|
447
|
-
predicate: (t) => {
|
|
448
|
-
return !existingThreadIds.has(t.id);
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
const th = ctx.discord.thread(thread.id);
|
|
452
|
-
// Wait for the final text after all 3 sequential tool steps
|
|
453
|
-
await waitForBotMessageContaining({
|
|
454
|
-
discord: ctx.discord,
|
|
455
|
-
threadId: thread.id,
|
|
456
|
-
userId: TEST_USER_ID,
|
|
457
|
-
text: 'chain complete: all 3 steps done',
|
|
458
|
-
timeout: 10_000,
|
|
459
|
-
});
|
|
460
|
-
// Wait for footer
|
|
461
|
-
await waitForFooterMessage({
|
|
462
|
-
discord: ctx.discord,
|
|
463
|
-
threadId: thread.id,
|
|
464
|
-
timeout: 6_000,
|
|
465
|
-
});
|
|
466
|
-
// Give any spurious extra footers time to arrive
|
|
467
|
-
await new Promise((resolve) => {
|
|
468
|
-
setTimeout(resolve, 500);
|
|
469
|
-
});
|
|
470
|
-
const messages = await th.getMessages();
|
|
471
|
-
const footerCount = messages.filter((m) => {
|
|
472
|
-
return m.author.id === ctx.discord.botUserId
|
|
473
|
-
&& m.content.startsWith('*')
|
|
474
|
-
&& m.content.includes('⋅');
|
|
475
|
-
}).length;
|
|
476
|
-
expect(await th.text()).toMatchInlineSnapshot(`
|
|
477
|
-
"--- from: user (queue-advanced-tester)
|
|
478
|
-
MULTI_STEP_CHAIN_MARKER
|
|
479
|
-
--- from: assistant (TestBot)
|
|
480
|
-
*using deterministic-provider/deterministic-v2*
|
|
481
|
-
⬥ chain step 1: reading config
|
|
482
|
-
┣ bash _echo chain-step-1-output_
|
|
483
|
-
⬥ chain step 2: analyzing results
|
|
484
|
-
┣ bash _echo chain-step-2-output_
|
|
485
|
-
⬥ chain step 3: applying fix
|
|
486
|
-
┣ bash _echo chain-step-3-output_
|
|
487
|
-
⬥ chain complete: all 3 steps done
|
|
488
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
489
|
-
`);
|
|
490
|
-
// The critical assertion: only 1 footer at the very end.
|
|
491
|
-
// With the naive "allow tool-calls as natural completion" fix,
|
|
492
|
-
// this would be 4 (one per assistant message). We want 1.
|
|
493
|
-
expect(footerCount).toBe(1);
|
|
494
|
-
}, 15_000);
|
|
495
289
|
});
|
|
@@ -252,7 +252,7 @@ describe('queue advanced: /model with interrupt recovery', () => {
|
|
|
252
252
|
--- from: assistant (TestBot)
|
|
253
253
|
*using deterministic-provider/deterministic-v2*
|
|
254
254
|
⬥ ok
|
|
255
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
255
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>
|
|
256
256
|
Model set for this session:
|
|
257
257
|
**Deterministic Provider** / **deterministic-v3**
|
|
258
258
|
\`deterministic-provider/deterministic-v3\`
|
|
@@ -267,7 +267,7 @@ describe('queue advanced: /model with interrupt recovery', () => {
|
|
|
267
267
|
Reply with exactly: model-switcher-followup
|
|
268
268
|
--- from: assistant (TestBot)
|
|
269
269
|
⬥ ok
|
|
270
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v3*"
|
|
270
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v3* <@200000000000000991>"
|
|
271
271
|
`);
|
|
272
272
|
expect(footer).toBeDefined();
|
|
273
273
|
expect(footer?.content).toContain('deterministic-v3');
|
|
@@ -32,7 +32,7 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
32
32
|
// every directory, so opt into the --restrict-directories behaviour here.
|
|
33
33
|
restrictExternalDirectories: true,
|
|
34
34
|
});
|
|
35
|
-
test('permission prompt pauses typing until user click, then typing resumes
|
|
35
|
+
test('permission prompt pauses typing until user click, then typing resumes', async () => {
|
|
36
36
|
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
37
37
|
content: 'PERMISSION_TYPING_MARKER',
|
|
38
38
|
});
|
|
@@ -56,7 +56,7 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
56
56
|
timeout: 4_000,
|
|
57
57
|
});
|
|
58
58
|
th.clearTypingEvents();
|
|
59
|
-
await th.waitForTypingEvent({ timeout:
|
|
59
|
+
await th.waitForTypingEvent({ timeout: 700 }).then(() => {
|
|
60
60
|
throw new Error('Typing should stay paused while permission UI is pending');
|
|
61
61
|
}, () => {
|
|
62
62
|
return undefined;
|
|
@@ -69,19 +69,19 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
69
69
|
interactionId: interaction.id,
|
|
70
70
|
timeout: 4_000,
|
|
71
71
|
});
|
|
72
|
-
const resumedTyping = await th.waitForTypingEvent({ timeout:
|
|
72
|
+
const resumedTyping = await th.waitForTypingEvent({ timeout: 2_000 });
|
|
73
73
|
expect(resumedTyping).toBeDefined();
|
|
74
74
|
await waitForBotMessageContaining({
|
|
75
75
|
discord: ctx.discord,
|
|
76
76
|
threadId: thread.id,
|
|
77
77
|
userId: TEST_USER_ID,
|
|
78
78
|
text: 'permission-flow-done',
|
|
79
|
-
timeout:
|
|
79
|
+
timeout: 4_000,
|
|
80
80
|
});
|
|
81
81
|
await waitForFooterMessage({
|
|
82
82
|
discord: ctx.discord,
|
|
83
83
|
threadId: thread.id,
|
|
84
|
-
timeout:
|
|
84
|
+
timeout: 4_000,
|
|
85
85
|
afterMessageIncludes: 'permission-flow-done',
|
|
86
86
|
afterAuthorId: ctx.discord.botUserId,
|
|
87
87
|
});
|
|
@@ -99,7 +99,7 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
99
99
|
✅ Permission **accepted**
|
|
100
100
|
[user clicks button]
|
|
101
101
|
⬥ permission-flow-done
|
|
102
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
102
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>"
|
|
103
103
|
`);
|
|
104
104
|
const timeline = await th.text({
|
|
105
105
|
showTyping: true,
|
|
@@ -112,11 +112,9 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
112
112
|
expect(donePosition).toBeGreaterThan(clickPosition);
|
|
113
113
|
expect(footerPosition).toBeGreaterThan(donePosition);
|
|
114
114
|
const afterClick = timeline.slice(clickPosition, donePosition);
|
|
115
|
-
const afterDone = timeline.slice(donePosition, footerPosition);
|
|
116
115
|
expect(afterClick).toContain('[bot typing]');
|
|
117
|
-
expect(afterDone).toContain('[bot typing]');
|
|
118
116
|
expect(timeline.slice(footerPosition)).not.toContain('[bot typing]');
|
|
119
|
-
},
|
|
117
|
+
}, 12_000);
|
|
120
118
|
test('manual thread message dismisses pending permission and sends the new prompt', async () => {
|
|
121
119
|
const initialPrompt = 'PERMISSION_TYPING_MARKER dismiss-flow';
|
|
122
120
|
const existingThreadIds = new Set((await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
|