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
|
@@ -2,7 +2,8 @@
|
|
|
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 {
|
|
5
|
+
import { store } from './store.js';
|
|
6
|
+
import { getMessageVisibleText, isFooterMessage, waitForFooterMessage, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, } from './test-utils.js';
|
|
6
7
|
const TEXT_CHANNEL_ID = '200000000000001001';
|
|
7
8
|
const e2eTest = describe;
|
|
8
9
|
e2eTest('queue advanced: footer emission', () => {
|
|
@@ -33,17 +34,48 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
33
34
|
"--- from: user (queue-advanced-tester)
|
|
34
35
|
Reply with exactly: footer-check
|
|
35
36
|
--- from: assistant (TestBot)
|
|
36
|
-
*using deterministic-provider/deterministic-v2*
|
|
37
|
-
|
|
38
|
-
*project ⋅ main ⋅
|
|
37
|
+
> *using deterministic-provider/deterministic-v2*
|
|
38
|
+
> ok
|
|
39
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
39
40
|
`);
|
|
40
41
|
const foundFooter = footerMessages.some((m) => {
|
|
41
42
|
return m.author.id === ctx.discord.botUserId
|
|
42
|
-
&& m.
|
|
43
|
-
&& m.content.includes('⋅');
|
|
43
|
+
&& isFooterMessage({ message: m, botUserId: ctx.discord.botUserId });
|
|
44
44
|
});
|
|
45
45
|
expect(foundFooter).toBe(true);
|
|
46
46
|
}, 8_000);
|
|
47
|
+
test('skip-footer-mentions omits the thread creator mention', async () => {
|
|
48
|
+
store.setState({ footerMentionsEnabled: false });
|
|
49
|
+
try {
|
|
50
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
51
|
+
content: 'Reply with exactly: footer-without-mention',
|
|
52
|
+
});
|
|
53
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
54
|
+
timeout: 4_000,
|
|
55
|
+
predicate: (t) => {
|
|
56
|
+
return t.name === 'Reply with exactly: footer-without-mention';
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const th = ctx.discord.thread(thread.id);
|
|
60
|
+
await th.waitForBotReply({ timeout: 4_000 });
|
|
61
|
+
await waitForFooterMessage({
|
|
62
|
+
discord: ctx.discord,
|
|
63
|
+
threadId: thread.id,
|
|
64
|
+
timeout: 4_000,
|
|
65
|
+
});
|
|
66
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
67
|
+
"--- from: user (queue-advanced-tester)
|
|
68
|
+
Reply with exactly: footer-without-mention
|
|
69
|
+
--- from: assistant (TestBot)
|
|
70
|
+
> *using deterministic-provider/deterministic-v2*
|
|
71
|
+
> ok
|
|
72
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*"
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
store.setState({ footerMentionsEnabled: true });
|
|
77
|
+
}
|
|
78
|
+
}, 8_000);
|
|
47
79
|
test('footer appears after second message in same session', async () => {
|
|
48
80
|
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
49
81
|
content: 'Reply with exactly: footer-multi-setup',
|
|
@@ -83,21 +115,20 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
83
115
|
const msgs = await th.getMessages();
|
|
84
116
|
const footerCount = msgs.filter((m) => {
|
|
85
117
|
return m.author.id === ctx.discord.botUserId
|
|
86
|
-
&& m.
|
|
87
|
-
&& m.content.includes('⋅');
|
|
118
|
+
&& isFooterMessage({ message: m, botUserId: ctx.discord.botUserId });
|
|
88
119
|
}).length;
|
|
89
120
|
expect(await th.text()).toMatchInlineSnapshot(`
|
|
90
121
|
"--- from: user (queue-advanced-tester)
|
|
91
122
|
Reply with exactly: footer-multi-setup
|
|
92
123
|
--- from: assistant (TestBot)
|
|
93
|
-
*using deterministic-provider/deterministic-v2*
|
|
94
|
-
|
|
95
|
-
*project ⋅ main ⋅
|
|
124
|
+
> *using deterministic-provider/deterministic-v2*
|
|
125
|
+
> ok
|
|
126
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
|
|
96
127
|
--- from: user (queue-advanced-tester)
|
|
97
128
|
Reply with exactly: footer-multi-second
|
|
98
129
|
--- from: assistant (TestBot)
|
|
99
|
-
|
|
100
|
-
*project ⋅ main ⋅
|
|
130
|
+
> ok
|
|
131
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
101
132
|
`);
|
|
102
133
|
if (footerCount >= 2) {
|
|
103
134
|
expect(footerCount).toBeGreaterThanOrEqual(2);
|
|
@@ -111,9 +142,7 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
111
142
|
});
|
|
112
143
|
const latestMsgs = await th.getMessages();
|
|
113
144
|
const count = latestMsgs.filter((m) => {
|
|
114
|
-
return m
|
|
115
|
-
&& m.content.startsWith('*')
|
|
116
|
-
&& m.content.includes('⋅');
|
|
145
|
+
return isFooterMessage({ message: m, botUserId: ctx.discord.botUserId });
|
|
117
146
|
}).length;
|
|
118
147
|
if (count >= 2) {
|
|
119
148
|
found = true;
|
|
@@ -174,7 +203,7 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
174
203
|
if (idx <= followupUserIdx) {
|
|
175
204
|
return false;
|
|
176
205
|
}
|
|
177
|
-
return m.author.id === ctx.discord.botUserId && m.
|
|
206
|
+
return m.author.id === ctx.discord.botUserId && getMessageVisibleText(m).includes('ok');
|
|
178
207
|
});
|
|
179
208
|
await waitForFooterMessage({
|
|
180
209
|
discord: ctx.discord,
|
|
@@ -187,18 +216,18 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
187
216
|
"--- from: user (queue-advanced-tester)
|
|
188
217
|
Reply with exactly: interrupt-footer-setup
|
|
189
218
|
--- from: assistant (TestBot)
|
|
190
|
-
*using deterministic-provider/deterministic-v2*
|
|
191
|
-
|
|
192
|
-
*project ⋅ main ⋅
|
|
219
|
+
> *using deterministic-provider/deterministic-v2*
|
|
220
|
+
> ok
|
|
221
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
|
|
193
222
|
--- from: user (queue-advanced-tester)
|
|
194
223
|
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
195
224
|
--- from: assistant (TestBot)
|
|
196
|
-
|
|
225
|
+
> starting sleep 100
|
|
197
226
|
--- from: user (queue-advanced-tester)
|
|
198
227
|
Reply with exactly: interrupt-footer-followup
|
|
199
228
|
--- from: assistant (TestBot)
|
|
200
|
-
|
|
201
|
-
*project ⋅ main ⋅
|
|
229
|
+
> ok
|
|
230
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
202
231
|
`);
|
|
203
232
|
expect(followupUserIdx).toBeGreaterThanOrEqual(0);
|
|
204
233
|
expect(okReplyIdx).toBeGreaterThan(followupUserIdx);
|
|
@@ -207,107 +236,11 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
207
236
|
return false;
|
|
208
237
|
}
|
|
209
238
|
return m.author.id === ctx.discord.botUserId
|
|
210
|
-
&& m.
|
|
211
|
-
&& m.content.includes('⋅');
|
|
239
|
+
&& isFooterMessage({ message: m, botUserId: ctx.discord.botUserId });
|
|
212
240
|
});
|
|
213
241
|
expect(footerBetween).toBe(false);
|
|
214
242
|
}, 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.
|
|
243
|
+
test('tool-call turn emits a footer after the final follow-up', async () => {
|
|
311
244
|
const existingThreadIds = new Set((await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
|
|
312
245
|
return thread.id;
|
|
313
246
|
}));
|
|
@@ -337,159 +270,17 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
337
270
|
threadId: thread.id,
|
|
338
271
|
timeout: 4_000,
|
|
339
272
|
});
|
|
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
273
|
expect(await th.text()).toMatchInlineSnapshot(`
|
|
361
274
|
"--- from: user (queue-advanced-tester)
|
|
362
275
|
TOOL_CALL_FOOTER_MARKER
|
|
363
276
|
--- from: assistant (TestBot)
|
|
364
|
-
*using deterministic-provider/deterministic-v2*
|
|
365
|
-
|
|
277
|
+
> *using deterministic-provider/deterministic-v2*
|
|
278
|
+
> running tool
|
|
279
|
+
|
|
366
280
|
┣ bash _echo tool-call-footer-test_
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
}, 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*"
|
|
281
|
+
|
|
282
|
+
> tool call completed
|
|
283
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
427
284
|
`);
|
|
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
285
|
}, 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
286
|
});
|
|
@@ -5,7 +5,7 @@ import { describe, test, expect } from 'vitest';
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
|
|
8
|
-
import { waitForBotMessageContaining, waitForBotReplyAfterUserMessage, waitForFooterMessage, waitForMessageById, } from './test-utils.js';
|
|
8
|
+
import { isFooterMessage, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, waitForFooterMessage, waitForMessageById, } from './test-utils.js';
|
|
9
9
|
import { getThreadState } from './session-handler/thread-runtime-state.js';
|
|
10
10
|
import { getSessionModel } from './database.js';
|
|
11
11
|
import { initializeOpencodeForDirectory } from './opencode.js';
|
|
@@ -242,17 +242,15 @@ describe('queue advanced: /model with interrupt recovery', () => {
|
|
|
242
242
|
afterAuthorId: TEST_USER_ID,
|
|
243
243
|
});
|
|
244
244
|
const footer = [...finalMessages].reverse().find((message) => {
|
|
245
|
-
return message
|
|
246
|
-
&& message.content.startsWith('*')
|
|
247
|
-
&& message.content.includes('⋅');
|
|
245
|
+
return isFooterMessage({ message, botUserId: ctx.discord.botUserId });
|
|
248
246
|
});
|
|
249
247
|
expect(await th.text()).toMatchInlineSnapshot(`
|
|
250
248
|
"--- from: user (queue-model-switch-tester)
|
|
251
249
|
Reply with exactly: model-switcher-setup
|
|
252
250
|
--- from: assistant (TestBot)
|
|
253
|
-
*using deterministic-provider/deterministic-v2*
|
|
254
|
-
|
|
255
|
-
*project ⋅ main ⋅
|
|
251
|
+
> *using deterministic-provider/deterministic-v2*
|
|
252
|
+
> ok
|
|
253
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
|
|
256
254
|
Model set for this session:
|
|
257
255
|
**Deterministic Provider** / **deterministic-v3**
|
|
258
256
|
\`deterministic-provider/deterministic-v3\`
|
|
@@ -261,13 +259,13 @@ describe('queue advanced: /model with interrupt recovery', () => {
|
|
|
261
259
|
--- from: user (queue-model-switch-tester)
|
|
262
260
|
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
263
261
|
--- from: assistant (TestBot)
|
|
264
|
-
|
|
265
|
-
|
|
262
|
+
> ok
|
|
263
|
+
> starting sleep 100
|
|
266
264
|
--- from: user (queue-model-switch-tester)
|
|
267
265
|
Reply with exactly: model-switcher-followup
|
|
268
266
|
--- from: assistant (TestBot)
|
|
269
|
-
|
|
270
|
-
*project ⋅ main ⋅
|
|
267
|
+
> ok
|
|
268
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v3* <@200000000000000991>"
|
|
271
269
|
`);
|
|
272
270
|
expect(footer).toBeDefined();
|
|
273
271
|
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
|
});
|
|
@@ -89,8 +89,9 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
89
89
|
"--- from: user (queue-permission-tester)
|
|
90
90
|
PERMISSION_TYPING_MARKER
|
|
91
91
|
--- from: assistant (TestBot)
|
|
92
|
-
*using deterministic-provider/deterministic-v2*
|
|
93
|
-
|
|
92
|
+
> *using deterministic-provider/deterministic-v2*
|
|
93
|
+
> requesting external read permission
|
|
94
|
+
|
|
94
95
|
┣ read *.zprofile*
|
|
95
96
|
⚠️ **Permission Required**
|
|
96
97
|
**Type:** \`external_directory\`
|
|
@@ -98,25 +99,24 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
98
99
|
**Pattern:** \`/Users/morse/*\`
|
|
99
100
|
✅ Permission **accepted**
|
|
100
101
|
[user clicks button]
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
|
|
103
|
+
> permission-flow-done
|
|
104
|
+
> *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
103
105
|
`);
|
|
104
106
|
const timeline = await th.text({
|
|
105
107
|
showTyping: true,
|
|
106
108
|
showInteractions: true,
|
|
107
109
|
});
|
|
108
110
|
const clickPosition = timeline.indexOf('[user clicks button]');
|
|
109
|
-
const donePosition = timeline.indexOf('
|
|
111
|
+
const donePosition = timeline.indexOf('permission-flow-done');
|
|
110
112
|
const footerPosition = timeline.lastIndexOf('*project ⋅');
|
|
111
113
|
expect(clickPosition).toBeGreaterThanOrEqual(0);
|
|
112
114
|
expect(donePosition).toBeGreaterThan(clickPosition);
|
|
113
115
|
expect(footerPosition).toBeGreaterThan(donePosition);
|
|
114
116
|
const afterClick = timeline.slice(clickPosition, donePosition);
|
|
115
|
-
const afterDone = timeline.slice(donePosition, footerPosition);
|
|
116
117
|
expect(afterClick).toContain('[bot typing]');
|
|
117
|
-
expect(afterDone).toContain('[bot typing]');
|
|
118
118
|
expect(timeline.slice(footerPosition)).not.toContain('[bot typing]');
|
|
119
|
-
},
|
|
119
|
+
}, 12_000);
|
|
120
120
|
test('manual thread message dismisses pending permission and sends the new prompt', async () => {
|
|
121
121
|
const initialPrompt = 'PERMISSION_TYPING_MARKER dismiss-flow';
|
|
122
122
|
const existingThreadIds = new Set((await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
|
|
@@ -175,12 +175,12 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
175
175
|
afterAuthorId: ctx.discord.botUserId,
|
|
176
176
|
});
|
|
177
177
|
const timeline = await th.text({ showInteractions: true });
|
|
178
|
-
const normalizedTimeline = timeline.replace('
|
|
178
|
+
const normalizedTimeline = timeline.replace('requesting external read permission\n', '');
|
|
179
179
|
expect(normalizedTimeline).toContain('PERMISSION_TYPING_MARKER dismiss-flow');
|
|
180
180
|
expect(normalizedTimeline).toContain('Permission dismissed - user sent a new message.');
|
|
181
181
|
expect(normalizedTimeline).toContain('Reply with exactly: post-permission-user-message');
|
|
182
182
|
const followupUserPosition = normalizedTimeline.indexOf('Reply with exactly: post-permission-user-message');
|
|
183
|
-
const followupReplyPosition = normalizedTimeline.indexOf('
|
|
183
|
+
const followupReplyPosition = normalizedTimeline.indexOf('ok', followupUserPosition);
|
|
184
184
|
const followupFooterPosition = normalizedTimeline.indexOf('*project ⋅', followupReplyPosition);
|
|
185
185
|
expect(followupUserPosition).toBeGreaterThanOrEqual(0);
|
|
186
186
|
expect(followupReplyPosition).toBeGreaterThan(followupUserPosition);
|
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
// get consumed as a tool result answer (which lost voice/image content).
|
|
5
5
|
import { describe, test, expect, afterEach } from 'vitest';
|
|
6
6
|
import { setupQueueAdvancedSuite, TEST_USER_ID } from './queue-advanced-e2e-setup.js';
|
|
7
|
-
import { waitForBotMessageContaining, waitForFooterMessage } from './test-utils.js';
|
|
7
|
+
import { waitForBotMessageContaining, waitForBotReplyAfterUserMessage, waitForFooterMessage, } from './test-utils.js';
|
|
8
8
|
import { store } from './store.js';
|
|
9
9
|
import { getOpencodeClient } from './opencode.js';
|
|
10
10
|
import { getThreadSession } from './database.js';
|
|
11
11
|
const TEXT_CHANNEL_ID = '200000000000001007';
|
|
12
|
-
const VOICE_CHANNEL_ID = '200000000000001017';
|
|
13
12
|
function setDeterministicTranscription(config) {
|
|
14
13
|
store.setState({
|
|
15
14
|
test: { deterministicTranscription: config },
|
|
@@ -128,45 +127,55 @@ describe('queue advanced: question tool answer', () => {
|
|
|
128
127
|
// 1. Dismiss the pending question (cleanup context)
|
|
129
128
|
// 2. Abort the blocked session so OpenCode unblocks
|
|
130
129
|
// 3. Enqueue the message as a normal user prompt (not consumed as answer)
|
|
130
|
+
const answer = 'Reply with exactly: question-text-answer-done';
|
|
131
131
|
await th.user(TEST_USER_ID).sendMessage({
|
|
132
|
-
content:
|
|
132
|
+
content: answer,
|
|
133
133
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
await waitForBotReplyAfterUserMessage({
|
|
135
|
+
discord: ctx.discord,
|
|
136
|
+
threadId: thread.id,
|
|
137
|
+
userId: TEST_USER_ID,
|
|
138
|
+
userMessageIncludes: answer,
|
|
139
|
+
timeout: 8_000,
|
|
140
|
+
});
|
|
141
|
+
await waitForBotMessageContaining({
|
|
142
|
+
discord: ctx.discord,
|
|
143
|
+
threadId: thread.id,
|
|
144
|
+
userId: TEST_USER_ID,
|
|
145
|
+
text: 'ok',
|
|
146
|
+
afterUserMessageIncludes: answer,
|
|
147
|
+
timeout: 8_000,
|
|
148
|
+
});
|
|
149
|
+
await waitForFooterMessage({
|
|
150
|
+
discord: ctx.discord,
|
|
151
|
+
threadId: thread.id,
|
|
152
|
+
timeout: 8_000,
|
|
153
|
+
afterMessageIncludes: 'ok',
|
|
154
|
+
afterAuthorId: ctx.discord.botUserId,
|
|
137
155
|
});
|
|
138
156
|
const timeline = await th.text({ showInteractions: true });
|
|
139
157
|
// The user's text answer must appear in Discord
|
|
140
|
-
expect(timeline).toContain(
|
|
158
|
+
expect(timeline).toContain(answer);
|
|
159
|
+
expect(timeline).toContain('ok');
|
|
141
160
|
// The original question must have appeared
|
|
142
161
|
expect(timeline).toContain('Which option do you prefer?');
|
|
143
162
|
// The user's marker message triggered the question
|
|
144
163
|
expect(timeline).toContain('QUESTION_TEXT_ANSWER_MARKER');
|
|
145
164
|
}, 20_000);
|
|
146
|
-
});
|
|
147
|
-
describe('queue advanced: voice message during pending question', () => {
|
|
148
|
-
const ctx = setupQueueAdvancedSuite({
|
|
149
|
-
channelId: VOICE_CHANNEL_ID,
|
|
150
|
-
channelName: 'qa-question-voice-e2e',
|
|
151
|
-
dirName: 'qa-question-voice-e2e',
|
|
152
|
-
username: 'queue-question-tester',
|
|
153
|
-
});
|
|
154
|
-
afterEach(() => {
|
|
155
|
-
setDeterministicTranscription(null);
|
|
156
|
-
});
|
|
157
165
|
test('voice message during pending question dismisses question and transcribes normally', async () => {
|
|
158
166
|
// This is the exact bug scenario: user sends a voice message while a
|
|
159
167
|
// question dropdown is pending. Voice messages have empty message.content
|
|
160
168
|
// (audio is in attachments, transcription happens later). The old code
|
|
161
169
|
// passed "" as the question answer and consumed the message — the voice
|
|
162
170
|
// content was completely lost.
|
|
163
|
-
|
|
164
|
-
|
|
171
|
+
const marker = 'QUESTION_TEXT_ANSWER_MARKER voice';
|
|
172
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
173
|
+
content: marker,
|
|
165
174
|
});
|
|
166
|
-
const thread = await ctx.discord.channel(
|
|
175
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
167
176
|
timeout: 8_000,
|
|
168
177
|
predicate: (t) => {
|
|
169
|
-
return t.name ===
|
|
178
|
+
return t.name === marker;
|
|
170
179
|
},
|
|
171
180
|
});
|
|
172
181
|
const th = ctx.discord.thread(thread.id);
|
|
@@ -188,10 +197,6 @@ describe('queue advanced: voice message during pending question', () => {
|
|
|
188
197
|
await th.user(TEST_USER_ID).sendVoiceMessage({
|
|
189
198
|
content: 'VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL',
|
|
190
199
|
});
|
|
191
|
-
// Give time for question cleanup to propagate
|
|
192
|
-
await new Promise((r) => {
|
|
193
|
-
setTimeout(r, 1_000);
|
|
194
|
-
});
|
|
195
200
|
// Voice content should be transcribed and appear as the next user message,
|
|
196
201
|
// processed after the model responds to the empty question answer.
|
|
197
202
|
await waitForBotMessageContaining({
|
|
@@ -254,7 +259,7 @@ describe('queue advanced: voice message during pending question', () => {
|
|
|
254
259
|
expect(timeline).toContain('VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL');
|
|
255
260
|
expect(timeline).toContain('🎤 Transcribing voice message...');
|
|
256
261
|
expect(timeline).toContain('📝 **Transcribed message:** I want option Alpha please');
|
|
257
|
-
expect(timeline).toContain('
|
|
262
|
+
expect(timeline).toContain('ok');
|
|
258
263
|
// Voice content must be present as a real transcribed message, not lost
|
|
259
264
|
expect(timeline).toContain('I want option Alpha please');
|
|
260
265
|
}, 20_000);
|