@@ -35,15 +35,15 @@ Codebuff is a tool for editing codebases via natural language instruction to Buf
3535
3636## Tool Handling System
3737
38- - Tools are defined in ` backend/src/tools.ts ` and implemented in ` npm-app/src/tool-handlers.ts `
38+ - Tools are defined in ` backend/src/tools/definitions/list .ts ` and implemented in ` npm-app/src/tool-handlers.ts `
3939- Available tools: read_files, write_file, str_replace, run_terminal_command, code_search, browser_logs, spawn_agents, web_search, read_docs, run_file_change_hooks, and others
4040- Backend uses tool calls to request additional information or perform actions
4141- Client-side handles tool calls and sends results back to server
4242
4343## Agent System
4444
45- - ** LLM-based Agents** : Traditional agents defined in ` backend/src/templates/ ` using prompts and LLM models
46- - ** Programmatic Agents** : Custom agents using JavaScript/TypeScript generator functions in ` .agents/templates/ `
45+ - ** LLM-based Agents** : Traditional agents defined in ` .agents/ ` subdirectories using prompts and LLM models
46+ - ** Programmatic Agents** : Custom agents using JavaScript/TypeScript generator functions in ` .agents/ `
4747- ** Dynamic Agent Templates** : User-defined agents in TypeScript files with ` handleSteps ` generator functions
4848- Agent templates define available tools, spawnable sub-agents, and execution behavior
4949- Programmatic agents allow complex orchestration logic, conditional flows, and iterative refinement
@@ -90,7 +90,6 @@ base-lite "fix this bug" # Works right away!
9090
9191## Error Handling and Debugging
9292
93- - The ` debug.ts ` file provides logging functionality for debugging
9493- Error messages are logged to console and debug log files
9594- WebSocket errors are caught and logged in server and client code
9695
@@ -101,166 +100,157 @@ base-lite "fix this bug" # Works right away!
101100- User input is validated and sanitized before processing
102101- File operations are restricted to project directory
103102
104- ## Testing Guidelines
103+ ## API Endpoint Architecture
105104
106- - Prefer specific imports over import \* to make dependencies explicit
107- - Exception: When mocking modules with many internal dependencies (like isomorphic-git), use import \* to avoid listing every internal function
105+ ### Dependency Injection Pattern
108106
109- ### Bun Testing Best Practices
107+ All API endpoints in ` web/src/app/api/v1/ ` follow a consistent dependency injection pattern for improved testability and maintainability.
110108
111- ** Always use ` spyOn() ` instead of ` mock.module() ` for function and method mocking. **
109+ ** Structure: **
112110
113- - When mocking modules is required (for the purposes of overriding constants instead of functions), use the wrapper functions found in ` @codebuff/common/testing/mock-modules.ts ` .
114- - ` mockModule ` is a drop-in replacement for ` mock.module ` , but the module should be the absolute module path (e.g., ` @codebuff/common/db ` instead of ` ../db ` ).
115- - Make sure to call ` clearMockedModules() ` in ` afterAll ` to restore the original module implementations.
111+ 1 . ** Implementation file** (` web/src/api/v1/<endpoint>.ts ` ) - Contains business logic with injected dependencies
112+ 2 . ** Route handler** (` web/src/app/api/v1/<endpoint>/route.ts ` ) - Minimal wrapper that injects dependencies
113+ 3 . ** Contract types** (` common/src/types/contracts/<domain>.ts ` ) - Type definitions for injected functions
114+ 4 . ** Unit tests** (` web/src/api/v1/__tests__/<endpoint>.test.ts ` ) - Comprehensive tests with mocked dependencies
116115
117- ** Preferred approach :**
116+ ** Example :**
118117
119118``` typescript
120- // ✅ Good: Use spyOn for clear, explicit mocking
121- import { spyOn , beforeEach , afterEach } from ' bun:test'
122- import * as analytics from ' ../analytics'
123-
124- beforeEach (() => {
125- // Spy on module functions
126- spyOn (analytics , ' trackEvent' ).mockImplementation (() => {})
127- spyOn (analytics , ' initAnalytics' ).mockImplementation (() => {})
128-
129- // Spy on global functions like Date.now and setTimeout
130- spyOn (Date , ' now' ).mockImplementation (() => 1234567890 )
131- spyOn (global , ' setTimeout' ).mockImplementation ((callback , delay ) => {
132- // Custom timeout logic for tests
133- return 123 as any
134- })
135- })
119+ // Implementation file - Contains business logic
120+ export async function myEndpoint(params : {
121+ req: NextRequest
122+ getDependency: GetDependencyFn
123+ logger: Logger
124+ anotherDep: AnotherDepFn
125+ }) {
126+ // Business logic here
127+ }
136128
137- afterEach (() => {
138- // Restore all mocks
139- mock .restore ()
140- })
129+ // Route handler - Minimal wrapper
130+ export async function GET(req : NextRequest ) {
131+ return myEndpointGet ({ req , getDependency , logger , anotherDep })
132+ }
133+
134+ // Contract type (in common/src/types/contracts/)
135+ export type GetDependencyFn = (params : SomeParams ) => Promise <SomeResult >
141136` ` `
142137
143- ** Real examples from our codebase:**
138+ **Benefits:**
139+
140+ - Easy to mock dependencies in unit tests
141+ - Type-safe function contracts shared across the codebase
142+ - Clear separation between routing and business logic
143+ - Consistent pattern across all endpoints
144+
145+ **Contract Types Location:**
146+ All contract types live in ` common /src /types /contracts /` .
147+
148+ **Contract Type Pattern:**
149+ For generic function types, use separate Input/Output types:
144150
145151` ` ` typescript
146- // From main-prompt.test.ts - Mocking LLM APIs
147- agentRuntimeImpl .promptAiSdk = async function () {
148- return ' Test response'
152+ // Define input type
153+ export type MyFunctionInput <T > = {
154+ param1: string
155+ param2: T
149156}
150- agentRuntimeImpl .promptAiSdkStream = async function * () {
151- yield { type: ' text' as const , text: ' Test response' }
152- return ' mock-message-id'
157+
158+ // Define output type
159+ export type MyFunctionOutput <T > = Promise <SomeResult <T >>
160+
161+ // Define function type using Input/Output
162+ export type MyFunctionFn = <T >(
163+ params : MyFunctionInput <T >,
164+ ) => MyFunctionOutput <T >
165+ ` ` `
166+
167+ ## Testing Guidelines
168+
169+ ### Dependency Injection (Primary Approach)
170+
171+ **Prefer dependency injection over mocking.** Design functions to accept dependencies as parameters with contract types defined in ` common /src /types /contracts /` .
172+
173+ ` ` ` typescript
174+ // ✅ Good: Dependency injection with contract types
175+ import type { TrackEventFn } from ' @codebuff/common/types/contracts/analytics'
176+ import type { Logger } from ' @codebuff/common/types/contracts/logger'
177+
178+ export async function myFunction(params : {
179+ trackEvent: TrackEventFn
180+ logger: Logger
181+ getData: GetDataFn
182+ }) {
183+ const { trackEvent, logger, getData } = params
184+ // Use injected dependencies
153185}
154186
155- // From rage-detector.test.ts - Mocking Date
156- spyOn (Date , ' now' ).mockImplementation (() => currentTime )
157-
158- // From run-agent-step-tools.test.ts - Mocking imported modules
159- spyOn (websocketAction , ' requestFiles' ).mockImplementation (
160- async (ws : any , paths : string []) => {
161- const results: Record <string , string | null > = {}
162- paths .forEach ((p ) => {
163- if (p === ' src/auth.ts' ) {
164- results [p ] = ' export function authenticate() { return true; }'
165- } else {
166- results [p ] = null
167- }
168- })
169- return results
170- },
171- )
187+ // Test with simple mock implementations
188+ const mockTrackEvent: TrackEventFn = mock (() => {})
189+ const mockLogger: Logger = {
190+ error: mock (() => {}),
191+ // ... other methods
192+ }
172193```
173194
174- ** Use ` mock.module() ` only for entire module replacement:**
195+ ** Benefits:**
196+
197+ - No need for ` spyOn() ` or ` mock.module() `
198+ - Clear, type-safe dependencies
199+ - Easy to test with simple mock objects
200+ - Better code architecture and maintainability
201+
202+ ### When to Use spyOn (Secondary Approach)
203+
204+ Use ` spyOn() ` only when dependency injection is impractical:
205+
206+ - Mocking global functions (Date.now, setTimeout)
207+ - Testing legacy code without DI
208+ - Overriding internal module behavior temporarily
175209
176210``` typescript
177- // ✅ Good: Use mock.module for replacing entire modules
178- mock .module (' ../util/logger' , () => ({
179- logger: {
180- debug : () => {},
181- error : () => {},
182- info : () => {},
183- warn : () => {},
184- },
185- withLoggerContext : async (context : any , fn : () => Promise <any >) => fn (),
186- }))
187-
188- // ✅ Good: Mock entire module with multiple exports using anonymous function
189- mock .module (' ../services/api-client' , () => ({
190- fetchUserData: jest .fn ().mockResolvedValue ({ id: 1 , name: ' Test User' }),
191- updateUserProfile: jest .fn ().mockResolvedValue ({ success: true }),
192- deleteUser: jest .fn ().mockResolvedValue (true ),
193- ApiError: class MockApiError extends Error {
194- constructor (
195- message : string ,
196- public status : number ,
197- ) {
198- super (message )
199- }
200- },
201- API_ENDPOINTS: {
202- USERS: ' /api/users' ,
203- PROFILES: ' /api/profiles' ,
204- },
205- }))
211+ // Use spyOn for globals
212+ spyOn (Date , ' now' ).mockImplementation (() => 1234567890 )
206213```
207214
208- ** Benefits of spyOn:**
215+ ### Avoid mock.module()
216+
217+ ** Never use ` mock.module() ` for functions.** It pollutes global state and carries over between test files.
209218
210- - Easier to restore original functionality with ` mock.restore() `
211- - Clearer test isolation
212- - Doesn't interfere with global state ( mock.module carries over from test file to test file, which is super bad and unintuitive.)
213- - Simpler debugging when mocks fail
219+ Only use for overriding module constants when absolutely necessary:
220+
221+ - Use wrapper functions in ` @codebuff/common/testing/ mock-modules.ts `
222+ - Call ` clearMockedModules() ` in ` afterAll `
214223
215224### Test Setup Patterns
216225
217- ** Extract duplicative mock state to ` beforeEach ` for cleaner tests: **
226+ Extract duplicative mock state to ` beforeEach ` :
218227
219228``` typescript
220- // ✅ Good: Extract common mock objects to beforeEach
221229describe (' My Tests' , () => {
222- let mockFileContext : ProjectFileContext
223- let mockAgentTemplate : DynamicAgentTemplate
230+ let mockLogger : Logger
231+ let mockTrackEvent : TrackEventFn
224232
225233 beforeEach (() => {
226- // Setup common mock data
227- mockFileContext = {
228- projectRoot: ' /test' ,
229- cwd: ' /test' ,
230- // ... other properties
231- }
232-
233- mockAgentTemplate = {
234- id: ' test-agent' ,
235- version: ' 1.0.0' ,
236- // ... other properties
234+ mockLogger = {
235+ error: mock (() => {}),
236+ warn: mock (() => {}),
237+ info: mock (() => {}),
238+ debug: mock (() => {}),
237239 }
240+ mockTrackEvent = mock (() => {})
238241 })
239242
240- test (' should work with mock data' , () => {
241- const agentTemplate = {
242- ' test-agent' : {
243- ... mockAgentTemplate ,
244- handleSteps: ' custom function' ,
245- } as any , // Use type assertion when needed
246- }
243+ afterEach (() => {
244+ mock .restore ()
245+ })
247246
248- const fileContext = {
249- ... mockFileContext ,
250- agentTemplates: agentTemplate ,
251- }
252- // ... test logic
247+ test (' works with injected dependencies' , async () => {
248+ await myFunction ({ logger: mockLogger , trackEvent: mockTrackEvent })
249+ expect (mockTrackEvent ).toHaveBeenCalled ()
253250 })
254251})
255252```
256253
257- ** Benefits:**
258-
259- - Reduces code duplication across tests
260- - Makes tests more maintainable
261- - Ensures consistent mock data structure
262- - Easier to update mock data in one place
263-
264254## Constants and Configuration
265255
266256Important constants are centralized in ` common/src/constants.ts ` :
0 commit comments