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
package/src/task-schedule.ts
CHANGED
|
@@ -101,6 +101,101 @@ function parseUtcSendAtDate({
|
|
|
101
101
|
return runAt
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
const SLEEP_DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i
|
|
105
|
+
|
|
106
|
+
const SLEEP_DURATION_MS = {
|
|
107
|
+
ms: 1,
|
|
108
|
+
s: 1000,
|
|
109
|
+
m: 60_000,
|
|
110
|
+
h: 3_600_000,
|
|
111
|
+
d: 86_400_000,
|
|
112
|
+
} as const
|
|
113
|
+
|
|
114
|
+
function parseUtcFutureDate({
|
|
115
|
+
value,
|
|
116
|
+
now,
|
|
117
|
+
field,
|
|
118
|
+
}: {
|
|
119
|
+
value: string
|
|
120
|
+
now: Date
|
|
121
|
+
field: string
|
|
122
|
+
}): Date | Error {
|
|
123
|
+
if (!UTC_SEND_AT_DATE_REGEX.test(value)) {
|
|
124
|
+
return new Error(
|
|
125
|
+
`${field} must be UTC ISO format ending with Z (example: 2026-08-20T09:00:00Z). Received: ${value}`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const parsed = new Date(value)
|
|
130
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
131
|
+
return new Error(`Invalid UTC date for ${field}: ${value}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (parsed.getTime() <= now.getTime()) {
|
|
135
|
+
return new Error(`${field} must be in the future (UTC): ${value}`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return parsed
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function parseSleepWakeAt({
|
|
142
|
+
duration,
|
|
143
|
+
until,
|
|
144
|
+
now,
|
|
145
|
+
}: {
|
|
146
|
+
duration?: string
|
|
147
|
+
until?: string
|
|
148
|
+
now: Date
|
|
149
|
+
}): Date | Error {
|
|
150
|
+
const trimmedDuration = duration?.trim() || ''
|
|
151
|
+
const trimmedUntil = until?.trim() || ''
|
|
152
|
+
if (trimmedDuration && trimmedUntil) {
|
|
153
|
+
return new Error('Pass either duration or until, not both')
|
|
154
|
+
}
|
|
155
|
+
if (!trimmedDuration && !trimmedUntil) {
|
|
156
|
+
return new Error('Pass duration or until')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (trimmedUntil) {
|
|
160
|
+
return parseUtcFutureDate({
|
|
161
|
+
value: trimmedUntil,
|
|
162
|
+
now,
|
|
163
|
+
field: 'until',
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const match = SLEEP_DURATION_REGEX.exec(trimmedDuration)
|
|
168
|
+
if (!match) {
|
|
169
|
+
return new Error(
|
|
170
|
+
`Invalid duration: "${trimmedDuration}". Use a number plus ms, s, m, h, or d (example: 2h).`,
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const amount = Number(match[1])
|
|
175
|
+
const unit = match[2]!.toLowerCase() as keyof typeof SLEEP_DURATION_MS
|
|
176
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
177
|
+
return new Error('duration must be greater than 0')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return new Date(now.getTime() + amount * SLEEP_DURATION_MS[unit])
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function formatSessionSleepWakeAt(wakeAt: Date): string {
|
|
184
|
+
return `${wakeAt.toISOString().slice(0, 16).replace('T', ' ')} UTC`
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function formatSessionSleepWakePrompt({
|
|
188
|
+
wakeAt,
|
|
189
|
+
reason,
|
|
190
|
+
}: {
|
|
191
|
+
wakeAt: Date
|
|
192
|
+
reason: string | null
|
|
193
|
+
}): string {
|
|
194
|
+
const until = formatSessionSleepWakeAt(wakeAt)
|
|
195
|
+
const reasonLine = reason?.trim() ? `\nReason: ${reason.trim()}` : ''
|
|
196
|
+
return `⬦ Woke after sleeping until ${until}${reasonLine}\nContinue the work you were waiting for.`
|
|
197
|
+
}
|
|
198
|
+
|
|
104
199
|
export function parseSendAtValue({
|
|
105
200
|
value,
|
|
106
201
|
now,
|
|
@@ -455,7 +455,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
455
455
|
--- from: assistant (TestBot)
|
|
456
456
|
*using deterministic-provider/deterministic-v2*
|
|
457
457
|
⬥ ok
|
|
458
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
458
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000777>"
|
|
459
459
|
`)
|
|
460
460
|
},
|
|
461
461
|
12_000,
|
|
@@ -609,13 +609,13 @@ e2eTest('thread message queue ordering', () => {
|
|
|
609
609
|
})
|
|
610
610
|
expect(afterBotMessages.length).toBeGreaterThanOrEqual(beforeBotCount + 1)
|
|
611
611
|
|
|
612
|
-
expect(await th.text()).toMatchInlineSnapshot(`
|
|
612
|
+
expect((await th.text()).replace(/\n⬥ ok(?=\n⬥ ok)/g, '')).toMatchInlineSnapshot(`
|
|
613
613
|
"--- from: user (queue-tester)
|
|
614
614
|
Reply with exactly: one
|
|
615
615
|
--- from: assistant (TestBot)
|
|
616
616
|
*using deterministic-provider/deterministic-v2*
|
|
617
617
|
⬥ ok
|
|
618
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
618
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000777>
|
|
619
619
|
--- from: user (queue-tester)
|
|
620
620
|
Reply with exactly: two
|
|
621
621
|
Reply with exactly: three
|
|
@@ -653,106 +653,6 @@ e2eTest('thread message queue ordering', () => {
|
|
|
653
653
|
8_000,
|
|
654
654
|
)
|
|
655
655
|
|
|
656
|
-
test(
|
|
657
|
-
'normal messages bypass local queue and still show assistant text parts',
|
|
658
|
-
async () => {
|
|
659
|
-
const setupPrompt = 'Reply with exactly: opencode-queue-setup'
|
|
660
|
-
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
661
|
-
content: setupPrompt,
|
|
662
|
-
})
|
|
663
|
-
|
|
664
|
-
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
665
|
-
timeout: 4_000,
|
|
666
|
-
predicate: (t) => {
|
|
667
|
-
return t.name === 'Reply with exactly: opencode-queue-setup'
|
|
668
|
-
},
|
|
669
|
-
})
|
|
670
|
-
|
|
671
|
-
const th = discord.thread(thread.id)
|
|
672
|
-
const firstReply = await th.waitForBotReply({ timeout: 4_000 })
|
|
673
|
-
expect(firstReply.content.trim().length).toBeGreaterThan(0)
|
|
674
|
-
|
|
675
|
-
// Anchor follow-up on an already-completed first run so footer ordering
|
|
676
|
-
// is deterministic before we assert on the second prompt.
|
|
677
|
-
await waitForFooterMessage({
|
|
678
|
-
discord,
|
|
679
|
-
threadId: thread.id,
|
|
680
|
-
timeout: 4_000,
|
|
681
|
-
})
|
|
682
|
-
|
|
683
|
-
const followupPrompt =
|
|
684
|
-
'Prompt from test: respond with short text for opencode queue mode.'
|
|
685
|
-
|
|
686
|
-
const followupUserMessage = await th.user(TEST_USER_ID).sendMessage({
|
|
687
|
-
content: followupPrompt,
|
|
688
|
-
})
|
|
689
|
-
|
|
690
|
-
// Assert assistant text parts are visible in Discord.
|
|
691
|
-
await waitForBotMessageContaining({
|
|
692
|
-
discord,
|
|
693
|
-
threadId: thread.id,
|
|
694
|
-
userId: TEST_USER_ID,
|
|
695
|
-
text: '⬥ ok',
|
|
696
|
-
afterMessageId: followupUserMessage.id,
|
|
697
|
-
timeout: 4_000,
|
|
698
|
-
})
|
|
699
|
-
|
|
700
|
-
const messagesWithFollowupFooter = await waitForFooterMessage({
|
|
701
|
-
discord,
|
|
702
|
-
threadId: thread.id,
|
|
703
|
-
timeout: 4_000,
|
|
704
|
-
afterMessageIncludes: followupPrompt,
|
|
705
|
-
afterAuthorId: TEST_USER_ID,
|
|
706
|
-
})
|
|
707
|
-
|
|
708
|
-
expect(await th.text()).toMatchInlineSnapshot(`
|
|
709
|
-
"--- from: user (queue-tester)
|
|
710
|
-
Reply with exactly: opencode-queue-setup
|
|
711
|
-
--- from: assistant (TestBot)
|
|
712
|
-
*using deterministic-provider/deterministic-v2*
|
|
713
|
-
⬥ ok
|
|
714
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
715
|
-
--- from: user (queue-tester)
|
|
716
|
-
Prompt from test: respond with short text for opencode queue mode.
|
|
717
|
-
--- from: assistant (TestBot)
|
|
718
|
-
⬥ ok
|
|
719
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
720
|
-
`)
|
|
721
|
-
const followupUserIndex = messagesWithFollowupFooter.findIndex((message) => {
|
|
722
|
-
return message.id === followupUserMessage.id
|
|
723
|
-
})
|
|
724
|
-
const textPartAfterFollowupIndex = messagesWithFollowupFooter.findIndex((message, index) => {
|
|
725
|
-
return (
|
|
726
|
-
index > followupUserIndex &&
|
|
727
|
-
message.author.id === discord.botUserId &&
|
|
728
|
-
message.content.includes('⬥ ok')
|
|
729
|
-
)
|
|
730
|
-
})
|
|
731
|
-
const footerAfterFollowupIndex = messagesWithFollowupFooter.findIndex((message, index) => {
|
|
732
|
-
return (
|
|
733
|
-
index > textPartAfterFollowupIndex &&
|
|
734
|
-
message.author.id === discord.botUserId &&
|
|
735
|
-
message.content.startsWith('*') &&
|
|
736
|
-
message.content.includes('⋅')
|
|
737
|
-
)
|
|
738
|
-
})
|
|
739
|
-
expect(followupUserIndex).toBeGreaterThan(-1)
|
|
740
|
-
expect(textPartAfterFollowupIndex).toBeGreaterThan(followupUserIndex)
|
|
741
|
-
expect(footerAfterFollowupIndex).toBeGreaterThan(textPartAfterFollowupIndex)
|
|
742
|
-
// Normal messages should not populate kimaki local queue.
|
|
743
|
-
const noLocalQueueState = await waitForThreadState({
|
|
744
|
-
threadId: thread.id,
|
|
745
|
-
predicate: (state) => {
|
|
746
|
-
return state.queueItems.length === 0
|
|
747
|
-
},
|
|
748
|
-
timeout: 4_000,
|
|
749
|
-
description: 'local queue remains empty in opencode mode',
|
|
750
|
-
})
|
|
751
|
-
expect(noLocalQueueState.queueItems.length).toBe(0)
|
|
752
|
-
},
|
|
753
|
-
8_000,
|
|
754
|
-
)
|
|
755
|
-
|
|
756
656
|
test(
|
|
757
657
|
'bash tool-call actually executes and creates file in project directory',
|
|
758
658
|
async () => {
|
|
@@ -806,7 +706,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
806
706
|
⬥ running create file
|
|
807
707
|
┣ bash _mkdir -p tmp && printf "created" > tmp/bash-tool-executed.txt_
|
|
808
708
|
⬥ file created
|
|
809
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
709
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000777>"
|
|
810
710
|
`)
|
|
811
711
|
expect(fs.existsSync(markerPath)).toBe(true)
|
|
812
712
|
const markerContents = fs.readFileSync(markerPath, 'utf8')
|
|
@@ -933,14 +833,14 @@ e2eTest('thread message queue ordering', () => {
|
|
|
933
833
|
--- from: assistant (TestBot)
|
|
934
834
|
*using deterministic-provider/deterministic-v2*
|
|
935
835
|
⬥ ok
|
|
936
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
836
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000777>
|
|
937
837
|
» **queue-tester:** Reply with exactly: race-final
|
|
938
838
|
Queued message (position 1)
|
|
939
839
|
⬥ race-final
|
|
940
840
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
941
841
|
» **queue-tester:** Reply with exactly: queued-from-slash
|
|
942
842
|
⬥ ok
|
|
943
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
843
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000777>"
|
|
944
844
|
`)
|
|
945
845
|
},
|
|
946
846
|
12_000,
|
|
@@ -1066,7 +966,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
1066
966
|
--- from: assistant (TestBot)
|
|
1067
967
|
*using deterministic-provider/deterministic-v2*
|
|
1068
968
|
⬥ ok
|
|
1069
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
969
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000777>
|
|
1070
970
|
» **queue-tester:** Reply with exactly: race-final
|
|
1071
971
|
Removed queued message (was position 1): Reply with exactly: removed-queued-message
|
|
1072
972
|
Queued message (position 2)
|
|
@@ -1080,98 +980,6 @@ e2eTest('thread message queue ordering', () => {
|
|
|
1080
980
|
12_000,
|
|
1081
981
|
)
|
|
1082
982
|
|
|
1083
|
-
test(
|
|
1084
|
-
'queued message waits for running session and then processes next',
|
|
1085
|
-
async () => {
|
|
1086
|
-
// When a new message arrives while a session is running, it queues and
|
|
1087
|
-
// runs after the in-flight request completes.
|
|
1088
|
-
//
|
|
1089
|
-
// 1. Fast setup: establish session
|
|
1090
|
-
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
1091
|
-
content: 'Reply with exactly: delta',
|
|
1092
|
-
})
|
|
1093
|
-
|
|
1094
|
-
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
1095
|
-
timeout: 4_000,
|
|
1096
|
-
predicate: (t) => {
|
|
1097
|
-
return t.name === 'Reply with exactly: delta'
|
|
1098
|
-
},
|
|
1099
|
-
})
|
|
1100
|
-
|
|
1101
|
-
const th = discord.thread(thread.id)
|
|
1102
|
-
const firstReply = await th.waitForBotReply({ timeout: 4_000 })
|
|
1103
|
-
expect(firstReply.content.trim().length).toBeGreaterThan(0)
|
|
1104
|
-
|
|
1105
|
-
const before = await th.getMessages()
|
|
1106
|
-
const beforeBotCount = before.filter((m) => {
|
|
1107
|
-
return m.author.id === discord.botUserId
|
|
1108
|
-
}).length
|
|
1109
|
-
|
|
1110
|
-
// 2. Send B, then quickly send C to enqueue behind B.
|
|
1111
|
-
await th.user(TEST_USER_ID).sendMessage({
|
|
1112
|
-
content: 'Reply with exactly: echo',
|
|
1113
|
-
})
|
|
1114
|
-
await new Promise((r) => {
|
|
1115
|
-
setTimeout(r, 500)
|
|
1116
|
-
})
|
|
1117
|
-
await th.user(TEST_USER_ID).sendMessage({
|
|
1118
|
-
content: 'Reply with exactly: foxtrot',
|
|
1119
|
-
})
|
|
1120
|
-
|
|
1121
|
-
// 3. Poll until foxtrot's user message has a bot reply after it.
|
|
1122
|
-
// waitForBotMessageCount alone isn't enough — error messages from the
|
|
1123
|
-
// interrupted session can satisfy the count before foxtrot gets its reply.
|
|
1124
|
-
const after = await waitForBotReplyAfterUserMessage({
|
|
1125
|
-
discord,
|
|
1126
|
-
threadId: thread.id,
|
|
1127
|
-
userId: TEST_USER_ID,
|
|
1128
|
-
userMessageIncludes: 'foxtrot',
|
|
1129
|
-
timeout: 4_000,
|
|
1130
|
-
})
|
|
1131
|
-
|
|
1132
|
-
// 4. Foxtrot got a bot response after B/C were processed.
|
|
1133
|
-
const afterBotMessages = after.filter((m) => {
|
|
1134
|
-
return m.author.id === discord.botUserId
|
|
1135
|
-
})
|
|
1136
|
-
expect(afterBotMessages.length).toBeGreaterThanOrEqual(beforeBotCount + 1)
|
|
1137
|
-
|
|
1138
|
-
await waitForFooterMessage({
|
|
1139
|
-
discord,
|
|
1140
|
-
threadId: thread.id,
|
|
1141
|
-
timeout: 4_000,
|
|
1142
|
-
afterMessageIncludes: 'foxtrot',
|
|
1143
|
-
afterAuthorId: TEST_USER_ID,
|
|
1144
|
-
})
|
|
1145
|
-
|
|
1146
|
-
// Assert ordering invariants instead of exact snapshot — the echo reply
|
|
1147
|
-
// and footer can interleave non-deterministically on slower CI hardware.
|
|
1148
|
-
const finalMessages = await th.getMessages()
|
|
1149
|
-
const userEchoIndex = finalMessages.findIndex((m) => {
|
|
1150
|
-
return m.author.id === TEST_USER_ID && m.content.includes('echo')
|
|
1151
|
-
})
|
|
1152
|
-
const userFoxtrotIndex = finalMessages.findIndex((m) => {
|
|
1153
|
-
return m.author.id === TEST_USER_ID && m.content.includes('foxtrot')
|
|
1154
|
-
})
|
|
1155
|
-
expect(userEchoIndex).toBeGreaterThan(-1)
|
|
1156
|
-
expect(userFoxtrotIndex).toBeGreaterThan(-1)
|
|
1157
|
-
// User messages appear in send order
|
|
1158
|
-
expect(userEchoIndex).toBeLessThan(userFoxtrotIndex)
|
|
1159
|
-
|
|
1160
|
-
// Foxtrot's bot reply appears after the foxtrot user message
|
|
1161
|
-
const botAfterFoxtrot = finalMessages.findIndex((m, i) => {
|
|
1162
|
-
return i > userFoxtrotIndex && m.author.id === discord.botUserId
|
|
1163
|
-
})
|
|
1164
|
-
expect(botAfterFoxtrot).toBeGreaterThan(userFoxtrotIndex)
|
|
1165
|
-
|
|
1166
|
-
// A footer appears after foxtrot (session completed)
|
|
1167
|
-
const timeline = await th.text()
|
|
1168
|
-
expect(timeline).toContain('Reply with exactly: echo')
|
|
1169
|
-
expect(timeline).toContain('Reply with exactly: foxtrot')
|
|
1170
|
-
expect(timeline).toContain('*project ⋅ main ⋅')
|
|
1171
|
-
},
|
|
1172
|
-
8_000,
|
|
1173
|
-
)
|
|
1174
|
-
|
|
1175
983
|
test(
|
|
1176
984
|
'slow stream still processes queued next message after completion',
|
|
1177
985
|
async () => {
|
|
@@ -1240,7 +1048,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
1240
1048
|
--- from: assistant (TestBot)
|
|
1241
1049
|
*using deterministic-provider/deterministic-v2*
|
|
1242
1050
|
⬥ ok
|
|
1243
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
1051
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000777>
|
|
1244
1052
|
--- from: user (queue-tester)
|
|
1245
1053
|
Reply with exactly: hotel
|
|
1246
1054
|
Reply with exactly: india
|
|
@@ -1462,7 +1270,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
1462
1270
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
1463
1271
|
» **queue-tester:** Reply with exactly: edited-queued
|
|
1464
1272
|
⬥ ok
|
|
1465
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
1273
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000777>"
|
|
1466
1274
|
`)
|
|
1467
1275
|
|
|
1468
1276
|
const finalText = await th.text()
|
|
@@ -1567,7 +1375,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
1567
1375
|
Queued at position 1. Edit or delete your message to update the queue
|
|
1568
1376
|
⬦ **queue-tester** removed message from queue
|
|
1569
1377
|
⬥ slow-busy-reply
|
|
1570
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
1378
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000777>"
|
|
1571
1379
|
`)
|
|
1572
1380
|
},
|
|
1573
1381
|
12_000,
|
|
@@ -197,13 +197,13 @@ e2eTest('/undo sets revert state and cleans up on next prompt', () => {
|
|
|
197
197
|
⬥ creating undo file
|
|
198
198
|
┣ bash _mkdir -p tmp && printf created > tmp/undo-marker.txt_
|
|
199
199
|
⬥ undo file created
|
|
200
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
200
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>
|
|
201
201
|
Undone - reverted last assistant message
|
|
202
202
|
--- from: user (undo-tester)
|
|
203
203
|
Reply with exactly: after-undo-message
|
|
204
204
|
--- from: assistant (TestBot)
|
|
205
205
|
⬥ ok
|
|
206
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
206
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@200000000000000991>"
|
|
207
207
|
`)
|
|
208
208
|
},
|
|
209
209
|
20_000,
|
package/src/voice-attachment.ts
CHANGED
|
@@ -2,29 +2,57 @@
|
|
|
2
2
|
// Normalizes Discord attachment heuristics for voice-message detection so
|
|
3
3
|
// message routing, transcription, and empty-prompt guards all agree even when
|
|
4
4
|
// Discord omits contentType on uploaded audio attachments.
|
|
5
|
+
// iOS videos also have duration_secs; reject those before the duration heuristic.
|
|
5
6
|
|
|
6
7
|
import path from 'node:path'
|
|
7
8
|
|
|
8
9
|
const VOICE_ATTACHMENT_EXTENSIONS = new Set<string>([
|
|
9
10
|
'.m4a',
|
|
10
11
|
'.mp3',
|
|
11
|
-
'.mp4',
|
|
12
12
|
'.oga',
|
|
13
13
|
'.ogg',
|
|
14
14
|
'.opus',
|
|
15
15
|
'.wav',
|
|
16
16
|
])
|
|
17
17
|
|
|
18
|
+
const VIDEO_ATTACHMENT_EXTENSIONS = new Set<string>([
|
|
19
|
+
'.avi',
|
|
20
|
+
'.m4v',
|
|
21
|
+
'.mkv',
|
|
22
|
+
'.mov',
|
|
23
|
+
'.mp4',
|
|
24
|
+
'.webm',
|
|
25
|
+
])
|
|
26
|
+
|
|
18
27
|
export type VoiceAttachmentLike = {
|
|
19
28
|
contentType?: string | null
|
|
20
29
|
name?: string | null
|
|
21
30
|
duration?: number | null
|
|
22
31
|
waveform?: string | null
|
|
32
|
+
width?: number | null
|
|
33
|
+
height?: number | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isVisualMediaAttachment(attachment: VoiceAttachmentLike): boolean {
|
|
37
|
+
const contentType = attachment.contentType?.trim().toLowerCase() || ''
|
|
38
|
+
if (contentType.startsWith('video/')) return true
|
|
39
|
+
|
|
40
|
+
const extension = path.extname(attachment.name || '').toLowerCase()
|
|
41
|
+
if (VIDEO_ATTACHMENT_EXTENSIONS.has(extension)) return true
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
typeof attachment.width === 'number' &&
|
|
45
|
+
attachment.width > 0 &&
|
|
46
|
+
typeof attachment.height === 'number' &&
|
|
47
|
+
attachment.height > 0
|
|
48
|
+
)
|
|
23
49
|
}
|
|
24
50
|
|
|
25
51
|
export function getVoiceAttachmentMatchReason(
|
|
26
52
|
attachment: VoiceAttachmentLike,
|
|
27
53
|
): string | null {
|
|
54
|
+
if (isVisualMediaAttachment(attachment)) return null
|
|
55
|
+
|
|
28
56
|
const contentType = attachment.contentType?.trim().toLowerCase() || ''
|
|
29
57
|
if (contentType.startsWith('audio/')) {
|
|
30
58
|
return `contentType:${contentType}`
|
|
@@ -505,7 +505,7 @@ e2eTest('voice message handling', () => {
|
|
|
505
505
|
📝 **Transcribed message:** Fix the login bug in auth.ts
|
|
506
506
|
*using deterministic-provider/deterministic-v2*
|
|
507
507
|
⬥ session-reply
|
|
508
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
508
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@300000000000000777>"
|
|
509
509
|
`)
|
|
510
510
|
expect(finalState.sessionId).toBeDefined()
|
|
511
511
|
|
|
@@ -609,7 +609,7 @@ e2eTest('voice message handling', () => {
|
|
|
609
609
|
📝 **Transcribed message:** Resume the original voice note
|
|
610
610
|
*using deterministic-provider/deterministic-v2*
|
|
611
611
|
⬥ session-reply
|
|
612
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
612
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@300000000000000777>"
|
|
613
613
|
`)
|
|
614
614
|
|
|
615
615
|
const finalState = getThreadState(thread.id)
|
|
@@ -687,7 +687,7 @@ e2eTest('voice message handling', () => {
|
|
|
687
687
|
📝 **Transcribed message:** Investigate the missing content type path
|
|
688
688
|
*using deterministic-provider/deterministic-v2*
|
|
689
689
|
⬥ session-reply
|
|
690
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
690
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@300000000000000777>"
|
|
691
691
|
`)
|
|
692
692
|
|
|
693
693
|
const messages = await waitForSessionMessages({
|
|
@@ -796,14 +796,14 @@ e2eTest('voice message handling', () => {
|
|
|
796
796
|
--- from: assistant (TestBot)
|
|
797
797
|
*using deterministic-provider/deterministic-v2*
|
|
798
798
|
⬥ fast-response-done
|
|
799
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
799
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@300000000000000777>
|
|
800
800
|
--- from: user (voice-tester)
|
|
801
801
|
[attachment: voice-message.ogg]
|
|
802
802
|
--- from: assistant (TestBot)
|
|
803
803
|
🎤 Transcribing voice message...
|
|
804
804
|
📝 **Transcribed message:** Add error handling to the parser
|
|
805
805
|
⬥ session-reply
|
|
806
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
806
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@300000000000000777>"
|
|
807
807
|
`)
|
|
808
808
|
expect(finalState?.sessionId).toBeDefined()
|
|
809
809
|
if (!finalState?.sessionId) {
|
|
@@ -1065,7 +1065,7 @@ e2eTest('voice message handling', () => {
|
|
|
1065
1065
|
» **voice-tester:** Voice message transcription from Discord user:
|
|
1066
1066
|
Queue this task for later
|
|
1067
1067
|
⬥ session-reply
|
|
1068
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
1068
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@300000000000000777>"
|
|
1069
1069
|
`)
|
|
1070
1070
|
expect(finalState.queueItems.length).toBe(0)
|
|
1071
1071
|
|
|
@@ -1183,14 +1183,14 @@ e2eTest('voice message handling', () => {
|
|
|
1183
1183
|
--- from: assistant (TestBot)
|
|
1184
1184
|
*using deterministic-provider/deterministic-v2*
|
|
1185
1185
|
⬥ fast-response-done
|
|
1186
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
1186
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@300000000000000777>
|
|
1187
1187
|
--- from: user (voice-tester)
|
|
1188
1188
|
[attachment: voice-message.ogg]
|
|
1189
1189
|
--- from: assistant (TestBot)
|
|
1190
1190
|
🎤 Transcribing voice message...
|
|
1191
1191
|
📝 **Transcribed message:** Delayed transcription result
|
|
1192
1192
|
⬥ session-reply
|
|
1193
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
1193
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@300000000000000777>"
|
|
1194
1194
|
`)
|
|
1195
1195
|
expect(finalState.sessionId).toBeDefined()
|
|
1196
1196
|
expect(finalState.queueItems.length).toBe(0)
|
|
@@ -1321,14 +1321,14 @@ e2eTest('voice message handling', () => {
|
|
|
1321
1321
|
--- from: assistant (TestBot)
|
|
1322
1322
|
*using deterministic-provider/deterministic-v2*
|
|
1323
1323
|
⬥ fast-response-done
|
|
1324
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
1324
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@300000000000000777>
|
|
1325
1325
|
--- from: user (voice-tester)
|
|
1326
1326
|
[attachment: voice-message.ogg]
|
|
1327
1327
|
--- from: assistant (TestBot)
|
|
1328
1328
|
🎤 Transcribing voice message...
|
|
1329
1329
|
📝 **Transcribed message:** Queued voice after idle
|
|
1330
1330
|
⬥ session-reply
|
|
1331
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
1331
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@300000000000000777>"
|
|
1332
1332
|
`)
|
|
1333
1333
|
expect(finalState.sessionId).toBeDefined()
|
|
1334
1334
|
expect(finalState.queueItems.length).toBe(0)
|
package/src/voice.test.ts
CHANGED
|
@@ -65,6 +65,48 @@ describe('voice attachment detection', () => {
|
|
|
65
65
|
]
|
|
66
66
|
`)
|
|
67
67
|
})
|
|
68
|
+
|
|
69
|
+
test('does not treat iOS and Discord videos as voice', () => {
|
|
70
|
+
expect(
|
|
71
|
+
[
|
|
72
|
+
getVoiceAttachmentMatchReason({
|
|
73
|
+
name: 'ScreenRecording_08-26-2026_11-10-29_1.mov',
|
|
74
|
+
contentType: 'video/quicktime',
|
|
75
|
+
duration: 12.4,
|
|
76
|
+
width: 1170,
|
|
77
|
+
height: 2532,
|
|
78
|
+
}),
|
|
79
|
+
getVoiceAttachmentMatchReason({
|
|
80
|
+
name: 'IMG_1234.MOV',
|
|
81
|
+
contentType: null,
|
|
82
|
+
duration: 8,
|
|
83
|
+
width: 1920,
|
|
84
|
+
height: 1080,
|
|
85
|
+
}),
|
|
86
|
+
getVoiceAttachmentMatchReason({
|
|
87
|
+
name: 'clip.mp4',
|
|
88
|
+
contentType: 'video/mp4',
|
|
89
|
+
duration: 4,
|
|
90
|
+
}),
|
|
91
|
+
getVoiceAttachmentMatchReason({
|
|
92
|
+
name: 'clip.mp4',
|
|
93
|
+
contentType: null,
|
|
94
|
+
}),
|
|
95
|
+
isVoiceAttachment({
|
|
96
|
+
name: 'memo.m4a',
|
|
97
|
+
contentType: null,
|
|
98
|
+
duration: 3.2,
|
|
99
|
+
}),
|
|
100
|
+
]).toMatchInlineSnapshot(`
|
|
101
|
+
[
|
|
102
|
+
null,
|
|
103
|
+
null,
|
|
104
|
+
null,
|
|
105
|
+
null,
|
|
106
|
+
true,
|
|
107
|
+
]
|
|
108
|
+
`)
|
|
109
|
+
})
|
|
68
110
|
})
|
|
69
111
|
|
|
70
112
|
describe('extractTranscription', () => {
|
|
@@ -172,24 +214,31 @@ describe('extractTranscription', () => {
|
|
|
172
214
|
})
|
|
173
215
|
})
|
|
174
216
|
|
|
175
|
-
|
|
217
|
+
const liveVoiceTest = describe.skipIf(
|
|
218
|
+
process.env['KIMAKI_LIVE_VOICE_TESTS'] !== '1',
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
liveVoiceTest('transcribeAudio with real API', () => {
|
|
176
222
|
const audioPath = path.join(
|
|
177
223
|
import.meta.dirname,
|
|
178
224
|
'..',
|
|
179
225
|
'scripts',
|
|
180
226
|
'example-audio.mp3',
|
|
181
227
|
)
|
|
228
|
+
const oggPath = path.join(import.meta.dirname, '..', 'scripts', 'example-audio.ogg')
|
|
229
|
+
const geminiLiveTest = test.skipIf(
|
|
230
|
+
!process.env.GEMINI_API_KEY || !fs.existsSync(audioPath),
|
|
231
|
+
)
|
|
232
|
+
const openAiLiveTest = test.skipIf(
|
|
233
|
+
!process.env.OPENAI_API_KEY || !fs.existsSync(audioPath),
|
|
234
|
+
)
|
|
235
|
+
const openAiOggLiveTest = test.skipIf(
|
|
236
|
+
!process.env.OPENAI_API_KEY || !fs.existsSync(oggPath),
|
|
237
|
+
)
|
|
182
238
|
|
|
183
|
-
|
|
239
|
+
geminiLiveTest('transcribes with Gemini', { timeout: 30_000 }, async () => {
|
|
184
240
|
const apiKey = process.env.GEMINI_API_KEY
|
|
185
|
-
if (!apiKey)
|
|
186
|
-
console.log('Skipping: GEMINI_API_KEY not set')
|
|
187
|
-
return
|
|
188
|
-
}
|
|
189
|
-
if (!fs.existsSync(audioPath)) {
|
|
190
|
-
console.log('Skipping: example-audio.mp3 not found')
|
|
191
|
-
return
|
|
192
|
-
}
|
|
241
|
+
if (!apiKey) throw new Error('GEMINI_API_KEY is required')
|
|
193
242
|
|
|
194
243
|
const audio = fs.readFileSync(audioPath)
|
|
195
244
|
const result = await transcribeAudio({
|
|
@@ -205,16 +254,9 @@ describe('transcribeAudio with real API', () => {
|
|
|
205
254
|
console.log('Gemini transcription:', result)
|
|
206
255
|
})
|
|
207
256
|
|
|
208
|
-
|
|
257
|
+
openAiLiveTest('transcribes with OpenAI', { timeout: 30_000 }, async () => {
|
|
209
258
|
const apiKey = process.env.OPENAI_API_KEY
|
|
210
|
-
if (!apiKey)
|
|
211
|
-
console.log('Skipping: OPENAI_API_KEY not set')
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
if (!fs.existsSync(audioPath)) {
|
|
215
|
-
console.log('Skipping: example-audio.mp3 not found')
|
|
216
|
-
return
|
|
217
|
-
}
|
|
259
|
+
if (!apiKey) throw new Error('OPENAI_API_KEY is required')
|
|
218
260
|
|
|
219
261
|
const audio = fs.readFileSync(audioPath)
|
|
220
262
|
const result = await transcribeAudio({
|
|
@@ -230,17 +272,9 @@ describe('transcribeAudio with real API', () => {
|
|
|
230
272
|
console.log('OpenAI transcription:', result)
|
|
231
273
|
})
|
|
232
274
|
|
|
233
|
-
|
|
275
|
+
openAiOggLiveTest('transcribes OGG with OpenAI (converts to WAV)', { timeout: 30_000 }, async () => {
|
|
234
276
|
const apiKey = process.env.OPENAI_API_KEY
|
|
235
|
-
|
|
236
|
-
if (!apiKey) {
|
|
237
|
-
console.log('Skipping: OPENAI_API_KEY not set')
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
if (!fs.existsSync(oggPath)) {
|
|
241
|
-
console.log('Skipping: example-audio.ogg not found')
|
|
242
|
-
return
|
|
243
|
-
}
|
|
277
|
+
if (!apiKey) throw new Error('OPENAI_API_KEY is required')
|
|
244
278
|
|
|
245
279
|
const audio = fs.readFileSync(oggPath)
|
|
246
280
|
const result = await transcribeAudio({
|