|
| 1 | +import { API_KEY_ENV_VAR } from '@codebuff/common/old-constants' |
| 2 | +import { describe, expect, it } from 'bun:test' |
| 3 | + |
| 4 | +import { CodebuffClient } from '@codebuff/sdk' |
| 5 | +import filePickerDefinition from '../file-explorer/file-picker' |
| 6 | +import fileListerDefinition from '../file-explorer/file-lister' |
| 7 | + |
| 8 | +import type { PrintModeEvent } from '@codebuff/common/types/print-mode' |
| 9 | + |
| 10 | +/** |
| 11 | + * Integration tests for agents that use the read_subtree tool. |
| 12 | + * These tests verify that the SDK properly initializes the session state |
| 13 | + * with project files and that agents can access the file tree through |
| 14 | + * the read_subtree tool. |
| 15 | + * |
| 16 | + * The file-lister agent is used directly instead of file-picker because: |
| 17 | + * - file-lister directly uses the read_subtree tool |
| 18 | + * - file-picker spawns file-lister as a subagent, adding complexity |
| 19 | + * - Testing file-lister directly verifies the core functionality |
| 20 | + */ |
| 21 | +describe('File Lister Agent Integration - read_subtree tool', () => { |
| 22 | + it( |
| 23 | + 'should find relevant files using read_subtree tool', |
| 24 | + async () => { |
| 25 | + const apiKey = process.env[API_KEY_ENV_VAR] |
| 26 | + if (!apiKey) { |
| 27 | + throw new Error('API key not found') |
| 28 | + } |
| 29 | + |
| 30 | + // Create mock project files that the file-lister should be able to find |
| 31 | + const projectFiles: Record<string, string> = { |
| 32 | + 'src/index.ts': ` |
| 33 | +import { UserService } from './services/user-service' |
| 34 | +import { AuthService } from './services/auth-service' |
| 35 | +
|
| 36 | +export function main() { |
| 37 | + const userService = new UserService() |
| 38 | + const authService = new AuthService() |
| 39 | + console.log('Application started') |
| 40 | +} |
| 41 | +`, |
| 42 | + 'src/services/user-service.ts': ` |
| 43 | +export class UserService { |
| 44 | + async getUser(id: string) { |
| 45 | + return { id, name: 'John Doe' } |
| 46 | + } |
| 47 | +
|
| 48 | + async createUser(name: string) { |
| 49 | + return { id: 'new-user-id', name } |
| 50 | + } |
| 51 | +
|
| 52 | + async deleteUser(id: string) { |
| 53 | + console.log('User deleted:', id) |
| 54 | + } |
| 55 | +} |
| 56 | +`, |
| 57 | + 'src/services/auth-service.ts': ` |
| 58 | +export class AuthService { |
| 59 | + async login(email: string, password: string) { |
| 60 | + return { token: 'mock-token' } |
| 61 | + } |
| 62 | +
|
| 63 | + async logout() { |
| 64 | + console.log('Logged out') |
| 65 | + } |
| 66 | +
|
| 67 | + async validateToken(token: string) { |
| 68 | + return token === 'mock-token' |
| 69 | + } |
| 70 | +} |
| 71 | +`, |
| 72 | + 'src/utils/logger.ts': ` |
| 73 | +export function log(message: string) { |
| 74 | + console.log('[LOG]', message) |
| 75 | +} |
| 76 | +
|
| 77 | +export function error(message: string) { |
| 78 | + console.error('[ERROR]', message) |
| 79 | +} |
| 80 | +`, |
| 81 | + 'src/types/user.ts': ` |
| 82 | +export interface User { |
| 83 | + id: string |
| 84 | + name: string |
| 85 | + email?: string |
| 86 | +} |
| 87 | +`, |
| 88 | + 'package.json': JSON.stringify({ |
| 89 | + name: 'test-project', |
| 90 | + version: '1.0.0', |
| 91 | + dependencies: {}, |
| 92 | + }), |
| 93 | + 'README.md': |
| 94 | + '# Test Project\n\nA simple test project for integration testing.', |
| 95 | + } |
| 96 | + |
| 97 | + const client = new CodebuffClient({ |
| 98 | + apiKey, |
| 99 | + cwd: '/tmp/test-project', |
| 100 | + projectFiles, |
| 101 | + }) |
| 102 | + |
| 103 | + const events: PrintModeEvent[] = [] |
| 104 | + |
| 105 | + // Run the file-lister agent to find files related to user service |
| 106 | + // The file-lister agent uses the read_subtree tool directly |
| 107 | + const run = await client.run({ |
| 108 | + agent: 'file-lister', |
| 109 | + prompt: 'Find files related to user authentication and user management', |
| 110 | + handleEvent: (event) => { |
| 111 | + events.push(event) |
| 112 | + }, |
| 113 | + }) |
| 114 | + |
| 115 | + // The output should not be an error |
| 116 | + expect(run.output.type).not.toEqual('error') |
| 117 | + |
| 118 | + // Verify we got some output |
| 119 | + expect(run.output).toBeDefined() |
| 120 | + |
| 121 | + // The file-lister should have found relevant files |
| 122 | + const outputStr = |
| 123 | + typeof run.output === 'string' ? run.output : JSON.stringify(run.output) |
| 124 | + |
| 125 | + // Verify that the file-lister found some relevant files |
| 126 | + const relevantFiles = [ |
| 127 | + 'user-service', |
| 128 | + 'auth-service', |
| 129 | + 'user', |
| 130 | + 'auth', |
| 131 | + 'services', |
| 132 | + ] |
| 133 | + const foundRelevantFile = relevantFiles.some((file) => |
| 134 | + outputStr.toLowerCase().includes(file.toLowerCase()), |
| 135 | + ) |
| 136 | + |
| 137 | + expect(foundRelevantFile).toBe(true) |
| 138 | + }, |
| 139 | + { timeout: 60_000 }, |
| 140 | + ) |
| 141 | + |
| 142 | + it( |
| 143 | + 'should use the file tree from session state', |
| 144 | + async () => { |
| 145 | + const apiKey = process.env[API_KEY_ENV_VAR] |
| 146 | + if (!apiKey) { |
| 147 | + throw new Error('API key not found') |
| 148 | + } |
| 149 | + |
| 150 | + // Create a different set of project files with a specific structure |
| 151 | + const projectFiles: Record<string, string> = { |
| 152 | + 'packages/core/src/index.ts': 'export const VERSION = "1.0.0"', |
| 153 | + 'packages/core/src/api/server.ts': |
| 154 | + 'export function startServer() { console.log("started") }', |
| 155 | + 'packages/core/src/api/routes.ts': |
| 156 | + 'export const routes = { health: "/health" }', |
| 157 | + 'packages/utils/src/helpers.ts': |
| 158 | + 'export function formatDate(d: Date) { return d.toISOString() }', |
| 159 | + 'docs/api.md': '# API Documentation\n\nAPI docs here.', |
| 160 | + 'package.json': JSON.stringify({ name: 'mono-repo', version: '2.0.0' }), |
| 161 | + } |
| 162 | + |
| 163 | + const client = new CodebuffClient({ |
| 164 | + apiKey, |
| 165 | + cwd: '/tmp/test-project', |
| 166 | + projectFiles, |
| 167 | + }) |
| 168 | + |
| 169 | + const events: PrintModeEvent[] = [] |
| 170 | + |
| 171 | + // Run file-lister to find API-related files |
| 172 | + const run = await client.run({ |
| 173 | + agent: 'file-lister', |
| 174 | + prompt: 'Find files related to the API server implementation', |
| 175 | + handleEvent: (event) => { |
| 176 | + events.push(event) |
| 177 | + }, |
| 178 | + }) |
| 179 | + |
| 180 | + expect(run.output.type).not.toEqual('error') |
| 181 | + |
| 182 | + const outputStr = |
| 183 | + typeof run.output === 'string' ? run.output : JSON.stringify(run.output) |
| 184 | + |
| 185 | + // Should find API-related files |
| 186 | + const apiRelatedTerms = ['server', 'routes', 'api', 'core'] |
| 187 | + const foundApiFile = apiRelatedTerms.some((term) => |
| 188 | + outputStr.toLowerCase().includes(term.toLowerCase()), |
| 189 | + ) |
| 190 | + |
| 191 | + expect(foundApiFile).toBe(true) |
| 192 | + }, |
| 193 | + { timeout: 60_000 }, |
| 194 | + ) |
| 195 | + |
| 196 | + it( |
| 197 | + 'should respect directories parameter', |
| 198 | + async () => { |
| 199 | + const apiKey = process.env[API_KEY_ENV_VAR] |
| 200 | + if (!apiKey) { |
| 201 | + throw new Error('API key not found') |
| 202 | + } |
| 203 | + |
| 204 | + // Create project with multiple top-level directories |
| 205 | + const projectFiles: Record<string, string> = { |
| 206 | + 'frontend/src/App.tsx': |
| 207 | + 'export function App() { return <div>App</div> }', |
| 208 | + 'frontend/src/components/Button.tsx': |
| 209 | + 'export function Button() { return <button>Click</button> }', |
| 210 | + 'backend/src/server.ts': |
| 211 | + 'export function start() { console.log("started") }', |
| 212 | + 'backend/src/routes/users.ts': |
| 213 | + 'export function getUsers() { return [] }', |
| 214 | + 'shared/types/common.ts': 'export type ID = string', |
| 215 | + 'package.json': JSON.stringify({ name: 'full-stack-app' }), |
| 216 | + } |
| 217 | + |
| 218 | + const client = new CodebuffClient({ |
| 219 | + apiKey, |
| 220 | + cwd: '/tmp/test-project', |
| 221 | + projectFiles, |
| 222 | + }) |
| 223 | + |
| 224 | + // Run file-lister with directories parameter to limit to frontend only |
| 225 | + const run = await client.run({ |
| 226 | + agent: 'file-lister', |
| 227 | + prompt: 'Find React component files', |
| 228 | + params: { |
| 229 | + directories: ['frontend'], |
| 230 | + }, |
| 231 | + handleEvent: () => {}, |
| 232 | + }) |
| 233 | + |
| 234 | + expect(run.output.type).not.toEqual('error') |
| 235 | + |
| 236 | + const outputStr = |
| 237 | + typeof run.output === 'string' ? run.output : JSON.stringify(run.output) |
| 238 | + |
| 239 | + // Should find frontend files |
| 240 | + const frontendTerms = ['app', 'button', 'component', 'frontend'] |
| 241 | + const foundFrontendFile = frontendTerms.some((term) => |
| 242 | + outputStr.toLowerCase().includes(term.toLowerCase()), |
| 243 | + ) |
| 244 | + |
| 245 | + expect(foundFrontendFile).toBe(true) |
| 246 | + }, |
| 247 | + { timeout: 60_000 }, |
| 248 | + ) |
| 249 | +}) |
| 250 | + |
| 251 | +/** |
| 252 | + * Integration tests for the file-picker agent that spawns subagents. |
| 253 | + * The file-picker spawns file-lister as a subagent to find files. |
| 254 | + * This tests the spawn_agents tool functionality through the SDK. |
| 255 | + */ |
| 256 | +describe('File Picker Agent Integration - spawn_agents tool', () => { |
| 257 | + // Note: This test requires the local agent definitions to be used for both |
| 258 | + // file-picker AND its spawned file-lister subagent. Currently, the spawned |
| 259 | + // agent may resolve to the server version which has the old parsing bug. |
| 260 | + // Skip until we have a way to ensure spawned agents use local definitions. |
| 261 | + it.skip( |
| 262 | + 'should spawn file-lister subagent and find relevant files', |
| 263 | + async () => { |
| 264 | + const apiKey = process.env[API_KEY_ENV_VAR] |
| 265 | + if (!apiKey) { |
| 266 | + throw new Error('API key not found') |
| 267 | + } |
| 268 | + |
| 269 | + // Create mock project files |
| 270 | + const projectFiles: Record<string, string> = { |
| 271 | + 'src/index.ts': ` |
| 272 | +import { UserService } from './services/user-service' |
| 273 | +export function main() { |
| 274 | + const userService = new UserService() |
| 275 | + console.log('Application started') |
| 276 | +} |
| 277 | +`, |
| 278 | + 'src/services/user-service.ts': ` |
| 279 | +export class UserService { |
| 280 | + async getUser(id: string) { |
| 281 | + return { id, name: 'John Doe' } |
| 282 | + } |
| 283 | +} |
| 284 | +`, |
| 285 | + 'src/services/auth-service.ts': ` |
| 286 | +export class AuthService { |
| 287 | + async login(email: string, password: string) { |
| 288 | + return { token: 'mock-token' } |
| 289 | + } |
| 290 | +} |
| 291 | +`, |
| 292 | + 'package.json': JSON.stringify({ |
| 293 | + name: 'test-project', |
| 294 | + version: '1.0.0', |
| 295 | + }), |
| 296 | + } |
| 297 | + |
| 298 | + // Use local agent definitions to test the updated handleSteps |
| 299 | + const localFilePickerDef = filePickerDefinition as unknown as any |
| 300 | + const localFileListerDef = fileListerDefinition as unknown as any |
| 301 | + |
| 302 | + const client = new CodebuffClient({ |
| 303 | + apiKey, |
| 304 | + cwd: '/tmp/test-project-picker', |
| 305 | + projectFiles, |
| 306 | + agentDefinitions: [localFilePickerDef, localFileListerDef], |
| 307 | + }) |
| 308 | + |
| 309 | + const events: PrintModeEvent[] = [] |
| 310 | + |
| 311 | + // Run the file-picker agent which spawns file-lister as a subagent |
| 312 | + const run = await client.run({ |
| 313 | + agent: localFilePickerDef.id, |
| 314 | + prompt: 'Find files related to user authentication', |
| 315 | + handleEvent: (event) => { |
| 316 | + events.push(event) |
| 317 | + }, |
| 318 | + }) |
| 319 | + |
| 320 | + // Check for errors in the output |
| 321 | + if (run.output.type === 'error') { |
| 322 | + console.error('File picker error:', run.output) |
| 323 | + } |
| 324 | + |
| 325 | + console.log('File picker output type:', run.output.type) |
| 326 | + console.log('File picker output:', JSON.stringify(run.output, null, 2)) |
| 327 | + |
| 328 | + // The output should not be an error |
| 329 | + expect(run.output.type).not.toEqual('error') |
| 330 | + |
| 331 | + // Verify we got some output |
| 332 | + expect(run.output).toBeDefined() |
| 333 | + |
| 334 | + // The file-picker should have found relevant files via its spawned file-lister |
| 335 | + const outputStr = |
| 336 | + typeof run.output === 'string' ? run.output : JSON.stringify(run.output) |
| 337 | + |
| 338 | + // Verify that the file-picker found some relevant files |
| 339 | + const relevantFiles = ['user', 'auth', 'service'] |
| 340 | + const foundRelevantFile = relevantFiles.some((file) => |
| 341 | + outputStr.toLowerCase().includes(file.toLowerCase()), |
| 342 | + ) |
| 343 | + |
| 344 | + expect(foundRelevantFile).toBe(true) |
| 345 | + }, |
| 346 | + { timeout: 90_000 }, |
| 347 | + ) |
| 348 | +}) |
0 commit comments