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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// E2e test: aborting a run must kill the pending question dropdown.
|
|
2
|
+
// Questions have no TTL, so abort is the only thing that clears them while the
|
|
3
|
+
// thread stays alive. Without this, a stale dropdown keeps accepting clicks and
|
|
4
|
+
// answers a question whose run is already dead.
|
|
5
|
+
import { describe, test, expect } from 'vitest';
|
|
6
|
+
import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
|
|
7
|
+
import { waitForBotMessageContaining } from './test-utils.js';
|
|
8
|
+
import { pendingQuestionContexts } from './commands/ask-question.js';
|
|
9
|
+
import { getRuntime } from './session-handler/thread-session-runtime.js';
|
|
10
|
+
const TEXT_CHANNEL_ID = '200000000000001031';
|
|
11
|
+
describe('abort clears pending question dropdown', () => {
|
|
12
|
+
const ctx = setupQueueAdvancedSuite({
|
|
13
|
+
channelId: TEXT_CHANNEL_ID,
|
|
14
|
+
channelName: 'qa-question-abort',
|
|
15
|
+
dirName: 'qa-question-abort',
|
|
16
|
+
username: 'question-abort-tester',
|
|
17
|
+
});
|
|
18
|
+
test('dropdown stops accepting answers after abort', async () => {
|
|
19
|
+
const marker = 'QUESTION_SELECT_QUEUE_MARKER abort-test';
|
|
20
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
21
|
+
content: marker,
|
|
22
|
+
});
|
|
23
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
24
|
+
timeout: 8_000,
|
|
25
|
+
predicate: (t) => {
|
|
26
|
+
return t.name === marker;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const th = ctx.discord.thread(thread.id);
|
|
30
|
+
const questionMessages = await waitForBotMessageContaining({
|
|
31
|
+
discord: ctx.discord,
|
|
32
|
+
threadId: thread.id,
|
|
33
|
+
text: 'How to proceed?',
|
|
34
|
+
timeout: 12_000,
|
|
35
|
+
});
|
|
36
|
+
const questionMsg = questionMessages.find((message) => {
|
|
37
|
+
return message.content.includes('How to proceed?');
|
|
38
|
+
});
|
|
39
|
+
if (!questionMsg) {
|
|
40
|
+
throw new Error('Expected question message');
|
|
41
|
+
}
|
|
42
|
+
const pendingEntry = [...pendingQuestionContexts.entries()].find(([, context]) => {
|
|
43
|
+
return context.thread.id === thread.id;
|
|
44
|
+
});
|
|
45
|
+
if (!pendingEntry) {
|
|
46
|
+
throw new Error('Expected pending question context');
|
|
47
|
+
}
|
|
48
|
+
const contextHash = pendingEntry[0];
|
|
49
|
+
const runtime = getRuntime(thread.id);
|
|
50
|
+
if (!runtime) {
|
|
51
|
+
throw new Error('Expected runtime for question abort test');
|
|
52
|
+
}
|
|
53
|
+
runtime.abortActiveRun('test-question-abort');
|
|
54
|
+
// Click the now-dead dropdown. The answer echo must never appear.
|
|
55
|
+
const interaction = await th.user(TEST_USER_ID).selectMenu({
|
|
56
|
+
messageId: questionMsg.id,
|
|
57
|
+
customId: `ask_question:${contextHash}:0`,
|
|
58
|
+
values: ['0'],
|
|
59
|
+
});
|
|
60
|
+
await th.waitForInteractionAck({
|
|
61
|
+
interactionId: interaction.id,
|
|
62
|
+
timeout: 8_000,
|
|
63
|
+
});
|
|
64
|
+
for (let i = 0; i < 10; i++) {
|
|
65
|
+
const messages = await th.getMessages();
|
|
66
|
+
const answered = messages.find((message) => {
|
|
67
|
+
return message.content.includes('» **question-abort-tester:** Alpha');
|
|
68
|
+
});
|
|
69
|
+
if (answered) {
|
|
70
|
+
throw new Error('Dropdown was still answerable after abort');
|
|
71
|
+
}
|
|
72
|
+
await new Promise((resolve) => {
|
|
73
|
+
setTimeout(resolve, 20);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const timeline = await th.text({ showInteractions: true });
|
|
77
|
+
expect(timeline).toMatchInlineSnapshot(`
|
|
78
|
+
"--- from: user (question-abort-tester)
|
|
79
|
+
QUESTION_SELECT_QUEUE_MARKER abort-test
|
|
80
|
+
--- from: assistant (TestBot)
|
|
81
|
+
> *using deterministic-provider/deterministic-v2*
|
|
82
|
+
**Select action**
|
|
83
|
+
How to proceed?
|
|
84
|
+
[user selects dropdown: 0]
|
|
85
|
+
This question has expired. Please ask the AI again."
|
|
86
|
+
`);
|
|
87
|
+
expect(pendingQuestionContexts.size).toBe(0);
|
|
88
|
+
}, 20_000);
|
|
89
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// E2e: leftover aborted text must post before a later question dropdown.
|
|
2
|
+
// Abort leaves an open text part buffered under the first assistant message.
|
|
3
|
+
// The retry is a new user turn, so a single-id / current-turn flush misses it.
|
|
4
|
+
import { describe, test, expect } from 'vitest';
|
|
5
|
+
import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
|
|
6
|
+
import { getRuntime } from './session-handler/thread-session-runtime.js';
|
|
7
|
+
import { getMessageVisibleText, waitForBotMessageContaining } from './test-utils.js';
|
|
8
|
+
const TEXT_CHANNEL_ID = '200000000000001043';
|
|
9
|
+
function createAbortRetryQuestionMatchers() {
|
|
10
|
+
const leftoverOpenTextMatcher = {
|
|
11
|
+
id: 'abort-retry-leftover-open-text',
|
|
12
|
+
priority: 130,
|
|
13
|
+
when: {
|
|
14
|
+
lastMessageRole: 'user',
|
|
15
|
+
latestUserTextIncludes: 'ABORT_RETRY_LEFTOVER_TEXT_MARKER',
|
|
16
|
+
},
|
|
17
|
+
then: {
|
|
18
|
+
parts: [
|
|
19
|
+
{ type: 'stream-start', warnings: [] },
|
|
20
|
+
{ type: 'text-start', id: 'abort-retry-started' },
|
|
21
|
+
{
|
|
22
|
+
type: 'text-delta',
|
|
23
|
+
id: 'abort-retry-started',
|
|
24
|
+
delta: 'abort-retry-started',
|
|
25
|
+
},
|
|
26
|
+
{ type: 'text-end', id: 'abort-retry-started' },
|
|
27
|
+
{ type: 'text-start', id: 'abort-retry-leftover' },
|
|
28
|
+
{
|
|
29
|
+
type: 'text-delta',
|
|
30
|
+
id: 'abort-retry-leftover',
|
|
31
|
+
delta: 'LEFTOVER_ABORTED_TEXT',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'finish',
|
|
35
|
+
finishReason: 'stop',
|
|
36
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
partDelaysMs: [0, 0, 0, 0, 0, 0, 100_000],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const questionAfterAbortMatcher = {
|
|
43
|
+
id: 'question-after-abort-retry',
|
|
44
|
+
priority: 131,
|
|
45
|
+
when: {
|
|
46
|
+
lastMessageRole: 'user',
|
|
47
|
+
latestUserTextIncludes: 'QUESTION_AFTER_ABORT_RETRY_MARKER',
|
|
48
|
+
},
|
|
49
|
+
then: {
|
|
50
|
+
parts: [
|
|
51
|
+
{ type: 'stream-start', warnings: [] },
|
|
52
|
+
{ type: 'text-start', id: 'plan-after-abort' },
|
|
53
|
+
{
|
|
54
|
+
type: 'text-delta',
|
|
55
|
+
id: 'plan-after-abort',
|
|
56
|
+
delta: 'PLAN_AFTER_ABORT_RETRY',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'tool-call',
|
|
60
|
+
toolCallId: 'question-after-abort-call',
|
|
61
|
+
toolName: 'question',
|
|
62
|
+
input: JSON.stringify({
|
|
63
|
+
questions: [{
|
|
64
|
+
question: 'What next after abort?',
|
|
65
|
+
header: 'Next step',
|
|
66
|
+
options: [
|
|
67
|
+
{ label: 'Commit', description: 'Commit these files' },
|
|
68
|
+
{ label: 'Stop', description: 'Leave uncommitted' },
|
|
69
|
+
],
|
|
70
|
+
}],
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
{ type: 'text-end', id: 'plan-after-abort' },
|
|
74
|
+
{
|
|
75
|
+
type: 'finish',
|
|
76
|
+
finishReason: 'tool-calls',
|
|
77
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
partDelaysMs: [200, 0, 0, 0, 400, 0],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
return [leftoverOpenTextMatcher, questionAfterAbortMatcher];
|
|
84
|
+
}
|
|
85
|
+
describe('question text before dropdown after abort retry', () => {
|
|
86
|
+
const ctx = setupQueueAdvancedSuite({
|
|
87
|
+
channelId: TEXT_CHANNEL_ID,
|
|
88
|
+
channelName: 'qa-question-abort-retry-order',
|
|
89
|
+
dirName: 'qa-question-abort-retry-order',
|
|
90
|
+
username: 'question-abort-retry-tester',
|
|
91
|
+
extraMatchers: createAbortRetryQuestionMatchers(),
|
|
92
|
+
});
|
|
93
|
+
test('posts leftover aborted text before the question dropdown', async () => {
|
|
94
|
+
const leftoverMarker = 'ABORT_RETRY_LEFTOVER_TEXT_MARKER';
|
|
95
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
96
|
+
content: leftoverMarker,
|
|
97
|
+
});
|
|
98
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
99
|
+
timeout: 8_000,
|
|
100
|
+
predicate: (t) => {
|
|
101
|
+
return t.name === leftoverMarker;
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const th = ctx.discord.thread(thread.id);
|
|
105
|
+
await waitForBotMessageContaining({
|
|
106
|
+
discord: ctx.discord,
|
|
107
|
+
threadId: thread.id,
|
|
108
|
+
text: 'abort-retry-started',
|
|
109
|
+
timeout: 8_000,
|
|
110
|
+
});
|
|
111
|
+
const runtime = getRuntime(thread.id);
|
|
112
|
+
expect(runtime).toBeDefined();
|
|
113
|
+
if (!runtime) {
|
|
114
|
+
throw new Error('Expected runtime for abort-retry question test');
|
|
115
|
+
}
|
|
116
|
+
runtime.abortActiveRun('test-question-abort-retry-flush');
|
|
117
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
118
|
+
content: 'QUESTION_AFTER_ABORT_RETRY_MARKER',
|
|
119
|
+
});
|
|
120
|
+
await waitForBotMessageContaining({
|
|
121
|
+
discord: ctx.discord,
|
|
122
|
+
threadId: thread.id,
|
|
123
|
+
text: 'What next after abort?',
|
|
124
|
+
timeout: 8_000,
|
|
125
|
+
});
|
|
126
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
127
|
+
"--- from: user (question-abort-retry-tester)
|
|
128
|
+
ABORT_RETRY_LEFTOVER_TEXT_MARKER
|
|
129
|
+
--- from: assistant (TestBot)
|
|
130
|
+
> *using deterministic-provider/deterministic-v2*
|
|
131
|
+
> abort-retry-started
|
|
132
|
+
--- from: user (question-abort-retry-tester)
|
|
133
|
+
QUESTION_AFTER_ABORT_RETRY_MARKER
|
|
134
|
+
--- from: assistant (TestBot)
|
|
135
|
+
> LEFTOVER_ABORTED_TEXT
|
|
136
|
+
PLAN_AFTER_ABORT_RETRY
|
|
137
|
+
**Next step**
|
|
138
|
+
What next after abort?"
|
|
139
|
+
`);
|
|
140
|
+
const messages = await th.getMessages();
|
|
141
|
+
const leftoverIndex = messages.findIndex((message) => {
|
|
142
|
+
return getMessageVisibleText(message).includes('LEFTOVER_ABORTED_TEXT');
|
|
143
|
+
});
|
|
144
|
+
const questionIndex = messages.findIndex((message) => {
|
|
145
|
+
return getMessageVisibleText(message).includes('What next after abort?');
|
|
146
|
+
});
|
|
147
|
+
expect(leftoverIndex).toBeGreaterThanOrEqual(0);
|
|
148
|
+
expect(questionIndex).toBeGreaterThan(leftoverIndex);
|
|
149
|
+
}, 15_000);
|
|
150
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// E2e: text that precedes a question tool must post before the dropdown.
|
|
2
|
+
// OpenCode can emit question.asked before the text part's time.end; Kimaki
|
|
3
|
+
// must wait so the plan is not hidden under the question UI.
|
|
4
|
+
import { describe, test, expect } from 'vitest';
|
|
5
|
+
import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
|
|
6
|
+
import { getMessageVisibleText, waitForBotMessageContaining } from './test-utils.js';
|
|
7
|
+
const TEXT_CHANNEL_ID = '200000000000001042';
|
|
8
|
+
describe('question text before dropdown', () => {
|
|
9
|
+
const ctx = setupQueueAdvancedSuite({
|
|
10
|
+
channelId: TEXT_CHANNEL_ID,
|
|
11
|
+
channelName: 'qa-question-text-order',
|
|
12
|
+
dirName: 'qa-question-text-order',
|
|
13
|
+
username: 'question-text-tester',
|
|
14
|
+
});
|
|
15
|
+
test('posts plan text before the question dropdown', async () => {
|
|
16
|
+
const marker = 'QUESTION_AFTER_TEXT_MARKER';
|
|
17
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
18
|
+
content: marker,
|
|
19
|
+
});
|
|
20
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
21
|
+
timeout: 8_000,
|
|
22
|
+
predicate: (t) => {
|
|
23
|
+
return t.name === marker;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const th = ctx.discord.thread(thread.id);
|
|
27
|
+
await waitForBotMessageContaining({
|
|
28
|
+
discord: ctx.discord,
|
|
29
|
+
threadId: thread.id,
|
|
30
|
+
text: 'What next?',
|
|
31
|
+
timeout: 8_000,
|
|
32
|
+
});
|
|
33
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
34
|
+
"--- from: user (question-text-tester)
|
|
35
|
+
QUESTION_AFTER_TEXT_MARKER
|
|
36
|
+
--- from: assistant (TestBot)
|
|
37
|
+
> *using deterministic-provider/deterministic-v2*
|
|
38
|
+
PLAN_TEXT_BEFORE_QUESTION
|
|
39
|
+
**Next step**
|
|
40
|
+
What next?"
|
|
41
|
+
`);
|
|
42
|
+
const messages = await th.getMessages();
|
|
43
|
+
const planIndex = messages.findIndex((message) => {
|
|
44
|
+
return getMessageVisibleText(message).includes('PLAN_TEXT_BEFORE_QUESTION');
|
|
45
|
+
});
|
|
46
|
+
const questionIndex = messages.findIndex((message) => {
|
|
47
|
+
return getMessageVisibleText(message).includes('What next?');
|
|
48
|
+
});
|
|
49
|
+
expect(planIndex).toBeGreaterThanOrEqual(0);
|
|
50
|
+
expect(questionIndex).toBeGreaterThan(planIndex);
|
|
51
|
+
}, 15_000);
|
|
52
|
+
});
|
|
@@ -5,7 +5,7 @@ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-set
|
|
|
5
5
|
import { getRuntime, } from './session-handler/thread-session-runtime.js';
|
|
6
6
|
import { getThreadState } from './session-handler/thread-runtime-state.js';
|
|
7
7
|
import { setSessionModel } from './database.js';
|
|
8
|
-
import { waitForFooterMessage, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, } from './test-utils.js';
|
|
8
|
+
import { isFooterMessage, waitForFooterMessage, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, } from './test-utils.js';
|
|
9
9
|
const TEXT_CHANNEL_ID = '200000000000001003';
|
|
10
10
|
const e2eTest = describe;
|
|
11
11
|
e2eTest('queue advanced: abort and retry', () => {
|
|
@@ -83,7 +83,7 @@ e2eTest('queue advanced: abort and retry', () => {
|
|
|
83
83
|
const timeline = await th.text();
|
|
84
84
|
expect(timeline).toContain('Reply with exactly: oscar');
|
|
85
85
|
expect(timeline).toContain('PLUGIN_TIMEOUT_SLEEP_MARKER');
|
|
86
|
-
expect(timeline).toContain('
|
|
86
|
+
expect(timeline).toContain('starting sleep 100');
|
|
87
87
|
expect(timeline).toContain('Reply with exactly: papa');
|
|
88
88
|
expect(timeline).toContain('*project ⋅ main ⋅');
|
|
89
89
|
// oscar comes before the sleep marker, sleep before papa
|
|
@@ -145,9 +145,7 @@ e2eTest('queue advanced: abort and retry', () => {
|
|
|
145
145
|
const msgs = await th.getMessages();
|
|
146
146
|
const newMsgs = msgs.slice(baselineCount);
|
|
147
147
|
const hasFooter = newMsgs.some((m) => {
|
|
148
|
-
return m
|
|
149
|
-
&& m.content.startsWith('*')
|
|
150
|
-
&& m.content.includes('⋅');
|
|
148
|
+
return isFooterMessage({ message: m, botUserId: ctx.discord.botUserId });
|
|
151
149
|
});
|
|
152
150
|
expect(hasFooter).toBe(false);
|
|
153
151
|
}
|
|
@@ -155,9 +153,9 @@ e2eTest('queue advanced: abort and retry', () => {
|
|
|
155
153
|
"--- from: user (queue-advanced-tester)
|
|
156
154
|
Reply with exactly: abort-no-footer-setup
|
|
157
155
|
--- from: assistant (TestBot)
|
|
158
|
-
*using deterministic-provider/deterministic-v2*
|
|
159
|
-
|
|
160
|
-
*project ⋅ main ⋅
|
|
156
|
+
> *using deterministic-provider/deterministic-v2*
|
|
157
|
+
> ok
|
|
158
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
|
|
161
159
|
--- from: user (queue-advanced-tester)
|
|
162
160
|
SLOW_ABORT_MARKER run long response"
|
|
163
161
|
`);
|
|
@@ -256,7 +254,7 @@ e2eTest('queue advanced: abort and retry', () => {
|
|
|
256
254
|
const text = await th.text();
|
|
257
255
|
// The follow-up reply ("ok") must be present with deterministic-v3
|
|
258
256
|
expect(text).toContain('Reply with exactly: model-switch-followup');
|
|
259
|
-
expect(text).toContain('
|
|
257
|
+
expect(text).toContain('ok');
|
|
260
258
|
// The old sleep text should be visible from the first turn
|
|
261
259
|
expect(text).toContain('starting sleep 100');
|
|
262
260
|
}, 10_000);
|
|
@@ -286,7 +284,7 @@ e2eTest('queue advanced: abort and retry', () => {
|
|
|
286
284
|
"--- from: user (queue-advanced-tester)
|
|
287
285
|
Reply with exactly: force-abort-setup
|
|
288
286
|
--- from: assistant (TestBot)
|
|
289
|
-
*using deterministic-provider/deterministic-v2*
|
|
287
|
+
> *using deterministic-provider/deterministic-v2*
|
|
290
288
|
--- from: user (queue-advanced-tester)
|
|
291
289
|
SLOW_ABORT_MARKER run long response"
|
|
292
290
|
`);
|
|
@@ -122,15 +122,15 @@ describe('queue advanced: action buttons', () => {
|
|
|
122
122
|
"--- from: user (queue-action-tester)
|
|
123
123
|
Reply with exactly: action-button-setup
|
|
124
124
|
--- from: assistant (TestBot)
|
|
125
|
-
*using deterministic-provider/deterministic-v2*
|
|
126
|
-
|
|
127
|
-
*project ⋅ main ⋅
|
|
125
|
+
> *using deterministic-provider/deterministic-v2*
|
|
126
|
+
> ok
|
|
127
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ 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
|
-
|
|
133
|
-
*project ⋅ main ⋅
|
|
132
|
+
> action-buttons-click-continued
|
|
133
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
134
134
|
`);
|
|
135
135
|
expect(timeline).toContain('action-buttons-click-continued');
|
|
136
136
|
}, 20_000);
|
|
@@ -195,9 +195,9 @@ describe('queue advanced: action buttons', () => {
|
|
|
195
195
|
"--- from: user (queue-action-tester)
|
|
196
196
|
Reply with exactly: action-button-dismiss-setup
|
|
197
197
|
--- from: assistant (TestBot)
|
|
198
|
-
*using deterministic-provider/deterministic-v2*
|
|
199
|
-
|
|
200
|
-
*project ⋅ main ⋅
|
|
198
|
+
> *using deterministic-provider/deterministic-v2*
|
|
199
|
+
> ok
|
|
200
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ 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 }
|