kimaki 0.4.76 → 0.4.78
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/adapter-rest-boundary.test.js +34 -0
- package/dist/agent-model.e2e.test.js +2 -20
- package/dist/cli.js +50 -13
- package/dist/commands/channel-ref.js +16 -0
- package/dist/commands/diff.js +20 -85
- package/dist/commands/merge-worktree.js +5 -17
- package/dist/commands/new-worktree.js +5 -9
- package/dist/commands/permissions.js +77 -11
- package/dist/commands/resume.js +5 -9
- package/dist/commands/screenshare.js +295 -0
- package/dist/commands/session.js +6 -17
- package/dist/critique-utils.js +95 -0
- package/dist/diff-patch-plugin.js +314 -0
- package/dist/discord-bot.js +19 -14
- package/dist/discord-js-import-boundary.test.js +62 -0
- package/dist/discord-utils.js +44 -0
- package/dist/event-stream-real-capture.e2e.test.js +2 -20
- package/dist/gateway-proxy.e2e.test.js +2 -5
- package/dist/generated/cloudflare/browser.js +17 -0
- package/dist/generated/cloudflare/client.js +34 -0
- package/dist/generated/cloudflare/commonInputTypes.js +10 -0
- package/dist/generated/cloudflare/enums.js +48 -0
- package/dist/generated/cloudflare/internal/class.js +47 -0
- package/dist/generated/cloudflare/internal/prismaNamespace.js +252 -0
- package/dist/generated/cloudflare/internal/prismaNamespaceBrowser.js +222 -0
- package/dist/generated/cloudflare/internal/query_compiler_fast_bg.js +135 -0
- package/dist/generated/cloudflare/models/bot_api_keys.js +1 -0
- package/dist/generated/cloudflare/models/bot_tokens.js +1 -0
- package/dist/generated/cloudflare/models/channel_agents.js +1 -0
- package/dist/generated/cloudflare/models/channel_directories.js +1 -0
- package/dist/generated/cloudflare/models/channel_mention_mode.js +1 -0
- package/dist/generated/cloudflare/models/channel_models.js +1 -0
- package/dist/generated/cloudflare/models/channel_verbosity.js +1 -0
- package/dist/generated/cloudflare/models/channel_worktrees.js +1 -0
- package/dist/generated/cloudflare/models/forum_sync_configs.js +1 -0
- package/dist/generated/cloudflare/models/global_models.js +1 -0
- package/dist/generated/cloudflare/models/ipc_requests.js +1 -0
- package/dist/generated/cloudflare/models/part_messages.js +1 -0
- package/dist/generated/cloudflare/models/scheduled_tasks.js +1 -0
- package/dist/generated/cloudflare/models/session_agents.js +1 -0
- package/dist/generated/cloudflare/models/session_events.js +1 -0
- package/dist/generated/cloudflare/models/session_models.js +1 -0
- package/dist/generated/cloudflare/models/session_start_sources.js +1 -0
- package/dist/generated/cloudflare/models/thread_sessions.js +1 -0
- package/dist/generated/cloudflare/models/thread_worktrees.js +1 -0
- package/dist/generated/cloudflare/models.js +1 -0
- package/dist/generated/node/browser.js +17 -0
- package/dist/generated/node/client.js +37 -0
- package/dist/generated/node/commonInputTypes.js +10 -0
- package/dist/generated/node/enums.js +48 -0
- package/dist/generated/node/internal/class.js +49 -0
- package/dist/generated/node/internal/prismaNamespace.js +252 -0
- package/dist/generated/node/internal/prismaNamespaceBrowser.js +222 -0
- package/dist/generated/node/models/bot_api_keys.js +1 -0
- package/dist/generated/node/models/bot_tokens.js +1 -0
- package/dist/generated/node/models/channel_agents.js +1 -0
- package/dist/generated/node/models/channel_directories.js +1 -0
- package/dist/generated/node/models/channel_mention_mode.js +1 -0
- package/dist/generated/node/models/channel_models.js +1 -0
- package/dist/generated/node/models/channel_verbosity.js +1 -0
- package/dist/generated/node/models/channel_worktrees.js +1 -0
- package/dist/generated/node/models/forum_sync_configs.js +1 -0
- package/dist/generated/node/models/global_models.js +1 -0
- package/dist/generated/node/models/ipc_requests.js +1 -0
- package/dist/generated/node/models/part_messages.js +1 -0
- package/dist/generated/node/models/scheduled_tasks.js +1 -0
- package/dist/generated/node/models/session_agents.js +1 -0
- package/dist/generated/node/models/session_events.js +1 -0
- package/dist/generated/node/models/session_models.js +1 -0
- package/dist/generated/node/models/session_start_sources.js +1 -0
- package/dist/generated/node/models/thread_sessions.js +1 -0
- package/dist/generated/node/models/thread_worktrees.js +1 -0
- package/dist/generated/node/models.js +1 -0
- package/dist/interaction-handler.js +10 -0
- package/dist/kimaki-digital-twin.e2e.test.js +2 -20
- package/dist/message-flags-boundary.test.js +54 -0
- package/dist/message-formatting.js +3 -62
- package/dist/onboarding-tutorial-plugin.js +1 -1
- package/dist/opencode-command.js +129 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +19 -1
- package/dist/opencode-interrupt-plugin.test.js +0 -5
- package/dist/opencode-plugin-loading.e2e.test.js +9 -20
- package/dist/opencode-plugin.js +4 -4
- package/dist/opencode.js +150 -27
- package/dist/patch-text-parser.js +97 -0
- package/dist/platform/components-v2.js +20 -0
- package/dist/platform/discord-adapter.js +1440 -0
- package/dist/platform/discord-routes.js +31 -0
- package/dist/platform/message-flags.js +8 -0
- package/dist/platform/platform-value.js +41 -0
- package/dist/platform/slack-adapter.js +872 -0
- package/dist/platform/slack-markdown.js +169 -0
- package/dist/platform/types.js +4 -0
- package/dist/queue-advanced-e2e-setup.js +265 -0
- package/dist/queue-advanced-footer.e2e.test.js +173 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +73 -1
- package/dist/runtime-lifecycle.e2e.test.js +2 -20
- package/dist/session-handler/event-stream-state.js +5 -0
- package/dist/session-handler/event-stream-state.test.js +6 -2
- package/dist/session-handler/thread-session-runtime.js +32 -2
- package/dist/system-message.js +26 -23
- package/dist/test-utils.js +16 -0
- package/dist/thread-message-queue.e2e.test.js +2 -20
- package/dist/utils.js +3 -1
- package/dist/voice-message.e2e.test.js +2 -20
- package/dist/voice.js +122 -9
- package/dist/voice.test.js +17 -2
- package/dist/websockify.js +69 -0
- package/dist/worktree-lifecycle.e2e.test.js +308 -0
- package/package.json +4 -2
- package/skills/critique/SKILL.md +17 -0
- package/skills/egaki/SKILL.md +35 -0
- package/skills/event-sourcing-state/SKILL.md +252 -0
- package/skills/goke/SKILL.md +1 -0
- package/skills/npm-package/SKILL.md +21 -2
- package/skills/playwriter/SKILL.md +1 -1
- package/skills/x-articles/SKILL.md +554 -0
- package/src/agent-model.e2e.test.ts +4 -19
- package/src/cli.ts +60 -13
- package/src/commands/diff.ts +25 -99
- package/src/commands/merge-worktree.ts +5 -21
- package/src/commands/new-worktree.ts +5 -11
- package/src/commands/permissions.ts +100 -15
- package/src/commands/resume.ts +5 -12
- package/src/commands/screenshare.ts +354 -0
- package/src/commands/session.ts +6 -23
- package/src/critique-utils.ts +139 -0
- package/src/discord-bot.ts +20 -15
- package/src/discord-utils.ts +53 -0
- package/src/event-stream-real-capture.e2e.test.ts +4 -20
- package/src/gateway-proxy.e2e.test.ts +2 -5
- package/src/interaction-handler.ts +15 -0
- package/src/kimaki-digital-twin.e2e.test.ts +2 -21
- package/src/message-formatting.ts +3 -68
- package/src/onboarding-tutorial-plugin.ts +1 -1
- package/src/opencode-command.test.ts +70 -0
- package/src/opencode-command.ts +188 -0
- package/src/opencode-interrupt-plugin.test.ts +0 -5
- package/src/opencode-interrupt-plugin.ts +34 -1
- package/src/opencode-plugin-loading.e2e.test.ts +25 -35
- package/src/opencode-plugin.ts +5 -4
- package/src/opencode.ts +199 -32
- package/src/patch-text-parser.ts +107 -0
- package/src/queue-advanced-e2e-setup.ts +273 -0
- package/src/queue-advanced-footer.e2e.test.ts +211 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +92 -0
- package/src/runtime-lifecycle.e2e.test.ts +4 -19
- package/src/session-handler/event-stream-state.test.ts +6 -2
- package/src/session-handler/event-stream-state.ts +5 -0
- package/src/session-handler/thread-session-runtime.ts +45 -2
- package/src/system-message.ts +26 -23
- package/src/test-utils.ts +17 -0
- package/src/thread-message-queue.e2e.test.ts +2 -20
- package/src/utils.ts +3 -1
- package/src/voice-message.e2e.test.ts +3 -20
- package/src/voice.test.ts +26 -2
- package/src/voice.ts +147 -9
- package/src/websockify.ts +101 -0
- package/src/worktree-lifecycle.e2e.test.ts +391 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// E2e test for worktree lifecycle: /new-worktree inside an existing thread,
|
|
2
|
+
// then verify the session still works after sdkDirectory switches.
|
|
3
|
+
// Validates that handleDirectoryChanged() reconnects the event listener
|
|
4
|
+
// so events from the worktree Instance reach the runtime (PR #75 fix).
|
|
5
|
+
//
|
|
6
|
+
// Uses opencode-deterministic-provider (no real LLM calls).
|
|
7
|
+
// Poll timeouts: 4s max, 100ms interval (except worktree creation which
|
|
8
|
+
// involves real git operations — 10s timeout there).
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs'
|
|
11
|
+
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import url from 'node:url'
|
|
14
|
+
import { describe, beforeAll, afterAll, test, expect } from 'vitest'
|
|
15
|
+
import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js'
|
|
16
|
+
import { DigitalDiscord } from 'discord-digital-twin/src'
|
|
17
|
+
import {
|
|
18
|
+
buildDeterministicOpencodeConfig,
|
|
19
|
+
type DeterministicMatcher,
|
|
20
|
+
} from 'opencode-deterministic-provider'
|
|
21
|
+
import { setDataDir } from './config.js'
|
|
22
|
+
import { store } from './store.js'
|
|
23
|
+
import { startDiscordBot } from './discord-bot.js'
|
|
24
|
+
import { getRuntime } from './session-handler/thread-session-runtime.js'
|
|
25
|
+
import {
|
|
26
|
+
setBotToken,
|
|
27
|
+
initDatabase,
|
|
28
|
+
closeDatabase,
|
|
29
|
+
setChannelDirectory,
|
|
30
|
+
setChannelVerbosity,
|
|
31
|
+
type VerbosityLevel,
|
|
32
|
+
} from './database.js'
|
|
33
|
+
import { startHranaServer, stopHranaServer } from './hrana-server.js'
|
|
34
|
+
import {
|
|
35
|
+
initializeOpencodeForDirectory,
|
|
36
|
+
stopOpencodeServer,
|
|
37
|
+
} from './opencode.js'
|
|
38
|
+
import {
|
|
39
|
+
chooseLockPort,
|
|
40
|
+
cleanupTestSessions,
|
|
41
|
+
waitForBotMessageContaining,
|
|
42
|
+
waitForBotReplyAfterUserMessage,
|
|
43
|
+
} from './test-utils.js'
|
|
44
|
+
import { execAsync } from './worktrees.js'
|
|
45
|
+
|
|
46
|
+
const TEST_USER_ID = '200000000000000901'
|
|
47
|
+
const TEXT_CHANNEL_ID = '200000000000000902'
|
|
48
|
+
// Unique worktree name per run to avoid collisions with leftover worktrees
|
|
49
|
+
const WORKTREE_SUFFIX = Date.now().toString(36).slice(-6)
|
|
50
|
+
const WORKTREE_NAME = `wt-e2e-${WORKTREE_SUFFIX}`
|
|
51
|
+
|
|
52
|
+
function createRunDirectories() {
|
|
53
|
+
const root = path.resolve(process.cwd(), 'tmp', 'worktree-lifecycle-e2e')
|
|
54
|
+
fs.mkdirSync(root, { recursive: true })
|
|
55
|
+
const dataDir = fs.mkdtempSync(path.join(root, 'data-'))
|
|
56
|
+
const projectDirectory = path.join(root, 'project')
|
|
57
|
+
fs.mkdirSync(projectDirectory, { recursive: true })
|
|
58
|
+
return { root, dataDir, projectDirectory }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createDiscordJsClient({ restUrl }: { restUrl: string }) {
|
|
62
|
+
return new Client({
|
|
63
|
+
intents: [
|
|
64
|
+
GatewayIntentBits.Guilds,
|
|
65
|
+
GatewayIntentBits.GuildMessages,
|
|
66
|
+
GatewayIntentBits.MessageContent,
|
|
67
|
+
GatewayIntentBits.GuildVoiceStates,
|
|
68
|
+
],
|
|
69
|
+
partials: [
|
|
70
|
+
Partials.Channel,
|
|
71
|
+
Partials.Message,
|
|
72
|
+
Partials.User,
|
|
73
|
+
Partials.ThreadMember,
|
|
74
|
+
],
|
|
75
|
+
rest: {
|
|
76
|
+
api: restUrl,
|
|
77
|
+
version: '10',
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Initialize a git repo with an initial commit so worktrees can be created. */
|
|
83
|
+
async function initGitRepo(directory: string): Promise<void> {
|
|
84
|
+
// Check if already a git repo (directory may persist across runs)
|
|
85
|
+
const isRepo = fs.existsSync(path.join(directory, '.git'))
|
|
86
|
+
if (isRepo) {
|
|
87
|
+
// Commit any new/changed files (opencode.json may have been rewritten)
|
|
88
|
+
await execAsync('git add -A && git diff --cached --quiet || git commit -m "update"', {
|
|
89
|
+
cwd: directory,
|
|
90
|
+
}).catch(() => { return })
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
await execAsync('git init', { cwd: directory })
|
|
94
|
+
await execAsync('git config user.email "test@test.com"', { cwd: directory })
|
|
95
|
+
await execAsync('git config user.name "Test"', { cwd: directory })
|
|
96
|
+
await execAsync('git add -A && git commit -m "initial"', { cwd: directory })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createDeterministicMatchers(): DeterministicMatcher[] {
|
|
100
|
+
const userReplyMatcher: DeterministicMatcher = {
|
|
101
|
+
id: 'user-reply',
|
|
102
|
+
priority: 10,
|
|
103
|
+
when: {
|
|
104
|
+
lastMessageRole: 'user',
|
|
105
|
+
rawPromptIncludes: 'Reply with exactly:',
|
|
106
|
+
},
|
|
107
|
+
then: {
|
|
108
|
+
parts: [
|
|
109
|
+
{ type: 'stream-start', warnings: [] },
|
|
110
|
+
{ type: 'text-start', id: 'default-reply' },
|
|
111
|
+
{ type: 'text-delta', id: 'default-reply', delta: 'ok' },
|
|
112
|
+
{ type: 'text-end', id: 'default-reply' },
|
|
113
|
+
{
|
|
114
|
+
type: 'finish',
|
|
115
|
+
finishReason: 'stop',
|
|
116
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
partDelaysMs: [0, 100, 0, 0, 0],
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return [userReplyMatcher]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
describe('worktree lifecycle', () => {
|
|
127
|
+
let directories: ReturnType<typeof createRunDirectories>
|
|
128
|
+
let discord: DigitalDiscord
|
|
129
|
+
let botClient: Client
|
|
130
|
+
let previousDefaultVerbosity: VerbosityLevel | null = null
|
|
131
|
+
let testStartTime = Date.now()
|
|
132
|
+
|
|
133
|
+
beforeAll(async () => {
|
|
134
|
+
testStartTime = Date.now()
|
|
135
|
+
directories = createRunDirectories()
|
|
136
|
+
const lockPort = chooseLockPort({ key: TEXT_CHANNEL_ID })
|
|
137
|
+
|
|
138
|
+
process.env['KIMAKI_LOCK_PORT'] = String(lockPort)
|
|
139
|
+
setDataDir(directories.dataDir)
|
|
140
|
+
previousDefaultVerbosity = store.getState().defaultVerbosity
|
|
141
|
+
store.setState({ defaultVerbosity: 'tools_and_text' })
|
|
142
|
+
|
|
143
|
+
const digitalDiscordDbPath = path.join(
|
|
144
|
+
directories.dataDir,
|
|
145
|
+
'digital-discord.db',
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
discord = new DigitalDiscord({
|
|
149
|
+
guild: {
|
|
150
|
+
name: 'Worktree E2E Guild',
|
|
151
|
+
ownerId: TEST_USER_ID,
|
|
152
|
+
},
|
|
153
|
+
channels: [
|
|
154
|
+
{
|
|
155
|
+
id: TEXT_CHANNEL_ID,
|
|
156
|
+
name: 'worktree-e2e',
|
|
157
|
+
type: ChannelType.GuildText,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
users: [
|
|
161
|
+
{
|
|
162
|
+
id: TEST_USER_ID,
|
|
163
|
+
username: 'worktree-tester',
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
dbUrl: `file:${digitalDiscordDbPath}`,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
await discord.start()
|
|
170
|
+
|
|
171
|
+
const providerNpm = url
|
|
172
|
+
.pathToFileURL(
|
|
173
|
+
path.resolve(
|
|
174
|
+
process.cwd(),
|
|
175
|
+
'..',
|
|
176
|
+
'opencode-deterministic-provider',
|
|
177
|
+
'src',
|
|
178
|
+
'index.ts',
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
.toString()
|
|
182
|
+
|
|
183
|
+
const opencodeConfig = buildDeterministicOpencodeConfig({
|
|
184
|
+
providerName: 'deterministic-provider',
|
|
185
|
+
providerNpm,
|
|
186
|
+
model: 'deterministic-v2',
|
|
187
|
+
smallModel: 'deterministic-v2',
|
|
188
|
+
settings: {
|
|
189
|
+
strict: false,
|
|
190
|
+
matchers: createDeterministicMatchers(),
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
fs.writeFileSync(
|
|
194
|
+
path.join(directories.projectDirectory, 'opencode.json'),
|
|
195
|
+
JSON.stringify(opencodeConfig, null, 2),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// Initialize git repo after writing opencode.json so the initial commit
|
|
199
|
+
// includes it. Worktrees require at least one commit.
|
|
200
|
+
await initGitRepo(directories.projectDirectory)
|
|
201
|
+
|
|
202
|
+
const dbPath = path.join(directories.dataDir, 'discord-sessions.db')
|
|
203
|
+
const hranaResult = await startHranaServer({ dbPath })
|
|
204
|
+
if (hranaResult instanceof Error) {
|
|
205
|
+
throw hranaResult
|
|
206
|
+
}
|
|
207
|
+
process.env['KIMAKI_DB_URL'] = hranaResult
|
|
208
|
+
await initDatabase()
|
|
209
|
+
await setBotToken(discord.botUserId, discord.botToken)
|
|
210
|
+
|
|
211
|
+
await setChannelDirectory({
|
|
212
|
+
channelId: TEXT_CHANNEL_ID,
|
|
213
|
+
directory: directories.projectDirectory,
|
|
214
|
+
channelType: 'text',
|
|
215
|
+
})
|
|
216
|
+
await setChannelVerbosity(TEXT_CHANNEL_ID, 'tools_and_text')
|
|
217
|
+
|
|
218
|
+
botClient = createDiscordJsClient({ restUrl: discord.restUrl })
|
|
219
|
+
await startDiscordBot({
|
|
220
|
+
token: discord.botToken,
|
|
221
|
+
appId: discord.botUserId,
|
|
222
|
+
discordClient: botClient,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// Pre-warm the opencode server
|
|
226
|
+
const warmup = await initializeOpencodeForDirectory(
|
|
227
|
+
directories.projectDirectory,
|
|
228
|
+
)
|
|
229
|
+
if (warmup instanceof Error) {
|
|
230
|
+
throw warmup
|
|
231
|
+
}
|
|
232
|
+
}, 60_000)
|
|
233
|
+
|
|
234
|
+
afterAll(async () => {
|
|
235
|
+
if (directories) {
|
|
236
|
+
await cleanupTestSessions({
|
|
237
|
+
projectDirectory: directories.projectDirectory,
|
|
238
|
+
testStartTime,
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
if (botClient) {
|
|
242
|
+
botClient.destroy()
|
|
243
|
+
}
|
|
244
|
+
await stopOpencodeServer()
|
|
245
|
+
await Promise.all([
|
|
246
|
+
closeDatabase().catch(() => { return }),
|
|
247
|
+
stopHranaServer().catch(() => { return }),
|
|
248
|
+
discord?.stop().catch(() => { return }),
|
|
249
|
+
])
|
|
250
|
+
delete process.env['KIMAKI_LOCK_PORT']
|
|
251
|
+
delete process.env['KIMAKI_DB_URL']
|
|
252
|
+
if (previousDefaultVerbosity) {
|
|
253
|
+
store.setState({ defaultVerbosity: previousDefaultVerbosity })
|
|
254
|
+
}
|
|
255
|
+
// Clean up the git worktree created during the test
|
|
256
|
+
if (directories) {
|
|
257
|
+
const worktreeBranch = `opencode/kimaki-${WORKTREE_NAME}`
|
|
258
|
+
await execAsync(
|
|
259
|
+
`git worktree list --porcelain`,
|
|
260
|
+
{ cwd: directories.projectDirectory },
|
|
261
|
+
).then(({ stdout }) => {
|
|
262
|
+
// Find and remove any worktree for our test branch
|
|
263
|
+
const lines = stdout.split('\n')
|
|
264
|
+
let currentPath = ''
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
if (line.startsWith('worktree ')) {
|
|
267
|
+
currentPath = line.slice('worktree '.length)
|
|
268
|
+
}
|
|
269
|
+
if (line.startsWith('branch ') && line.includes(worktreeBranch) && currentPath) {
|
|
270
|
+
return execAsync(
|
|
271
|
+
`git worktree remove --force ${JSON.stringify(currentPath)}`,
|
|
272
|
+
{ cwd: directories.projectDirectory },
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}).catch(() => { return })
|
|
277
|
+
await execAsync(
|
|
278
|
+
`git branch -D ${JSON.stringify(`opencode/kimaki-${WORKTREE_NAME}`)}`,
|
|
279
|
+
{ cwd: directories.projectDirectory },
|
|
280
|
+
).catch(() => { return })
|
|
281
|
+
fs.rmSync(directories.dataDir, { recursive: true, force: true })
|
|
282
|
+
}
|
|
283
|
+
}, 10_000)
|
|
284
|
+
|
|
285
|
+
test(
|
|
286
|
+
'session responds after /new-worktree switches sdkDirectory in existing thread',
|
|
287
|
+
async () => {
|
|
288
|
+
// 1. Send a message to create a thread and establish a session
|
|
289
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
290
|
+
content: 'Reply with exactly: before-worktree',
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
294
|
+
timeout: 4_000,
|
|
295
|
+
predicate: (t) => {
|
|
296
|
+
return t.name === 'Reply with exactly: before-worktree'
|
|
297
|
+
},
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
const th = discord.thread(thread.id)
|
|
301
|
+
|
|
302
|
+
// Wait for first run to fully complete (footer appears)
|
|
303
|
+
await waitForBotMessageContaining({
|
|
304
|
+
discord,
|
|
305
|
+
threadId: thread.id,
|
|
306
|
+
userId: TEST_USER_ID,
|
|
307
|
+
text: '*project',
|
|
308
|
+
timeout: 4_000,
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// Capture runtime — should survive the directory switch
|
|
312
|
+
const runtimeBefore = getRuntime(thread.id)
|
|
313
|
+
expect(runtimeBefore).toBeDefined()
|
|
314
|
+
expect(runtimeBefore!.sdkDirectory).toBe(directories.projectDirectory)
|
|
315
|
+
|
|
316
|
+
// 2. Run /new-worktree inside the thread (in-thread flow).
|
|
317
|
+
// This creates a pending worktree, then background creates the git worktree,
|
|
318
|
+
// then marks it ready. Next message will pick up the worktree directory.
|
|
319
|
+
const { id: interactionId } = await th
|
|
320
|
+
.user(TEST_USER_ID)
|
|
321
|
+
.runSlashCommand({
|
|
322
|
+
name: 'new-worktree',
|
|
323
|
+
options: [{ name: 'name', type: 3, value: WORKTREE_NAME }],
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// Wait for the slash command ack
|
|
327
|
+
await discord
|
|
328
|
+
.channel(thread.id)
|
|
329
|
+
.waitForInteractionAck({ interactionId, timeout: 4_000 })
|
|
330
|
+
|
|
331
|
+
// 3. Wait for worktree to become ready — the background creation
|
|
332
|
+
// edits the starter message to include the branch name.
|
|
333
|
+
// Git worktree creation involves real git operations, so allow more time.
|
|
334
|
+
await waitForBotMessageContaining({
|
|
335
|
+
discord,
|
|
336
|
+
threadId: thread.id,
|
|
337
|
+
userId: TEST_USER_ID,
|
|
338
|
+
text: 'Branch:',
|
|
339
|
+
timeout: 10_000,
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// 4. Send a message after the worktree is ready.
|
|
343
|
+
// Without handleDirectoryChanged (PR #75), the event listener is still
|
|
344
|
+
// subscribed to the old project directory's Instance, so this message
|
|
345
|
+
// gets processed but the response events never reach the runtime.
|
|
346
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
347
|
+
content: 'Reply with exactly: after-worktree',
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// 5. Verify the bot actually responds — this is the core assertion.
|
|
351
|
+
// If the listener wasn't reconnected, this will time out.
|
|
352
|
+
await waitForBotReplyAfterUserMessage({
|
|
353
|
+
discord,
|
|
354
|
+
threadId: thread.id,
|
|
355
|
+
userId: TEST_USER_ID,
|
|
356
|
+
userMessageIncludes: 'after-worktree',
|
|
357
|
+
timeout: 4_000,
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
// Wait for the footer to confirm full completion
|
|
361
|
+
await waitForBotMessageContaining({
|
|
362
|
+
discord,
|
|
363
|
+
threadId: thread.id,
|
|
364
|
+
userId: TEST_USER_ID,
|
|
365
|
+
text: 'deterministic-v2',
|
|
366
|
+
afterUserMessageIncludes: 'after-worktree',
|
|
367
|
+
timeout: 4_000,
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
// Runtime instance should be the same (not recreated)
|
|
371
|
+
const runtimeAfter = getRuntime(thread.id)
|
|
372
|
+
expect(runtimeAfter).toBe(runtimeBefore)
|
|
373
|
+
|
|
374
|
+
// sdkDirectory should now point to the worktree path
|
|
375
|
+
expect(runtimeAfter!.sdkDirectory).not.toBe(directories.projectDirectory)
|
|
376
|
+
expect(runtimeAfter!.sdkDirectory).toContain(`kimaki-${WORKTREE_NAME}`)
|
|
377
|
+
|
|
378
|
+
// Snapshot uses dynamic worktree name so we verify structure, not exact text
|
|
379
|
+
const text = await th.text()
|
|
380
|
+
expect(text).toContain('Reply with exactly: before-worktree')
|
|
381
|
+
expect(text).toContain('⬥ ok')
|
|
382
|
+
expect(text).toContain('Worktree:')
|
|
383
|
+
expect(text).toContain('Branch:')
|
|
384
|
+
expect(text).toContain('Reply with exactly: after-worktree')
|
|
385
|
+
// The second "⬥ ok" proves the bot responded after the worktree switch
|
|
386
|
+
const okCount = (text.match(/⬥ ok/g) || []).length
|
|
387
|
+
expect(okCount).toBe(2)
|
|
388
|
+
},
|
|
389
|
+
30_000,
|
|
390
|
+
)
|
|
391
|
+
})
|