Skip to content

Commit 9a5a07b

Browse files
committed
sdk: Fix auto-generation of project files if none provided. Add unit tests for initial session state
1 parent 98fcd57 commit 9a5a07b

File tree

4 files changed

+290
-9
lines changed

4 files changed

+290
-9
lines changed

bun.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import { describe, expect, test, beforeEach } from 'bun:test'
2+
import { z } from 'zod/v4'
3+
import { initialSessionState } from '../run-state'
4+
import type { CodebuffFileSystem } from '@codebuff/common/types/filesystem'
5+
6+
describe('Initial Session State', () => {
7+
let mockFs: CodebuffFileSystem
8+
let mockLogger: any
9+
10+
beforeEach(() => {
11+
mockFs = {
12+
readFile: async (path: string) => {
13+
if (path.includes('src/index.ts')) {
14+
return 'console.log("Hello world");'
15+
}
16+
if (path.includes('src/utils.ts')) {
17+
return 'export function add(a: number, b: number) { return a + b; }'
18+
}
19+
if (path.includes('knowledge.md')) {
20+
return '# Knowledge\n\nThis is a knowledge file.'
21+
}
22+
if (path.includes('README.md')) {
23+
return '# Project\n\nThis is a readme.'
24+
}
25+
if (path.includes('.gitignore')) {
26+
return 'node_modules/\n.git/'
27+
}
28+
throw new Error(`File not found: ${path}`)
29+
},
30+
readdir: async (path: string) => {
31+
if (path.includes('test-project')) {
32+
return [
33+
{ name: 'src', isDirectory: () => true, isFile: () => false },
34+
{ name: '.git', isDirectory: () => true, isFile: () => false },
35+
{ name: 'knowledge.md', isDirectory: () => false, isFile: () => true },
36+
{ name: 'README.md', isDirectory: () => false, isFile: () => true },
37+
{ name: '.gitignore', isDirectory: () => false, isFile: () => true },
38+
] as any
39+
}
40+
if (path.includes('src')) {
41+
return [
42+
{ name: 'index.ts', isDirectory: () => false, isFile: () => true },
43+
{ name: 'utils.ts', isDirectory: () => false, isFile: () => true },
44+
] as any
45+
}
46+
return []
47+
},
48+
stat: async (path: string) => ({
49+
isDirectory: () => path.includes('src') || path.includes('.git'),
50+
isFile: () => !path.includes('src') && !path.includes('.git'),
51+
}) as any,
52+
exists: async (path: string) => {
53+
if (path.includes('.gitignore')) return true
54+
if (path.includes('src')) return true
55+
if (path.includes('.git')) return true
56+
if (path.includes('knowledge.md')) return true
57+
if (path.includes('README.md')) return true
58+
return false
59+
},
60+
} as any
61+
62+
mockLogger = {
63+
debug: () => {},
64+
info: () => {},
65+
warn: () => {},
66+
error: () => {},
67+
}
68+
})
69+
70+
test('creates initial session state with explicit projectFiles', async () => {
71+
const projectFiles = {
72+
'src/index.ts': 'console.log("Hello world");',
73+
'src/utils.ts': 'export function add(a: number, b: number) { return a + b; }',
74+
'knowledge.md': '# Knowledge\n\nThis is a knowledge file.',
75+
}
76+
77+
const sessionState = await initialSessionState({
78+
cwd: '/test-project',
79+
projectFiles,
80+
fs: mockFs,
81+
logger: mockLogger,
82+
})
83+
84+
expect(sessionState.fileContext.fileTree).toBeDefined()
85+
expect(sessionState.fileContext.fileTree.length).toBeGreaterThan(0)
86+
expect(sessionState.fileContext.fileTokenScores).toBeDefined()
87+
expect(sessionState.mainAgentState.agentId).toBe('main-agent')
88+
expect(sessionState.mainAgentState.messageHistory).toEqual([])
89+
})
90+
91+
test('discovers project files automatically when projectFiles is undefined', async () => {
92+
const sessionState = await initialSessionState({
93+
cwd: '/test-project',
94+
projectFiles: undefined,
95+
fs: mockFs,
96+
logger: mockLogger,
97+
})
98+
99+
expect(sessionState.fileContext.fileTree).toBeDefined()
100+
expect(sessionState.mainAgentState.agentId).toBe('main-agent')
101+
expect(sessionState.mainAgentState.messageHistory).toEqual([])
102+
})
103+
104+
test('derives knowledgeFiles from projectFiles when not provided', async () => {
105+
const projectFiles = {
106+
'src/index.ts': 'console.log("Hello world");',
107+
'knowledge.md': '# Knowledge\n\nThis is a knowledge file.',
108+
'claude.md': '# Claude context\n\nThis is claude context.',
109+
'README.md': '# Project\n\nThis is a readme.',
110+
}
111+
112+
const sessionState = await initialSessionState({
113+
cwd: '/test-project',
114+
projectFiles,
115+
knowledgeFiles: undefined,
116+
fs: mockFs,
117+
logger: mockLogger,
118+
})
119+
120+
expect(sessionState.fileContext.knowledgeFiles).toBeDefined()
121+
expect(sessionState.fileContext.knowledgeFiles['knowledge.md']).toBe(
122+
'# Knowledge\n\nThis is a knowledge file.',
123+
)
124+
expect(sessionState.fileContext.knowledgeFiles['claude.md']).toBe(
125+
'# Claude context\n\nThis is claude context.',
126+
)
127+
expect(sessionState.fileContext.knowledgeFiles['README.md']).toBeUndefined()
128+
})
129+
130+
test('respects explicit knowledgeFiles when provided', async () => {
131+
const projectFiles = {
132+
'src/index.ts': 'console.log("Hello world");',
133+
'knowledge.md': '# Knowledge\n\nThis is a knowledge file.',
134+
}
135+
136+
const knowledgeFiles = {
137+
'custom-knowledge.md': '# Custom Knowledge\n\nThis is custom knowledge.',
138+
}
139+
140+
const sessionState = await initialSessionState({
141+
cwd: '/test-project',
142+
projectFiles,
143+
knowledgeFiles,
144+
fs: mockFs,
145+
logger: mockLogger,
146+
})
147+
148+
expect(sessionState.fileContext.knowledgeFiles).toEqual(knowledgeFiles)
149+
expect(sessionState.fileContext.knowledgeFiles['knowledge.md']).toBeUndefined()
150+
})
151+
152+
test('sets maxAgentSteps when provided', async () => {
153+
const projectFiles = {
154+
'src/index.ts': 'console.log("Hello world");',
155+
}
156+
157+
const sessionState = await initialSessionState({
158+
cwd: '/test-project',
159+
projectFiles,
160+
maxAgentSteps: 10,
161+
fs: mockFs,
162+
logger: mockLogger,
163+
})
164+
165+
expect(sessionState.mainAgentState.stepsRemaining).toBe(10)
166+
})
167+
168+
test('includes custom agent definitions', async () => {
169+
const projectFiles = {
170+
'src/index.ts': 'console.log("Hello world");',
171+
}
172+
173+
const agentDefinitions = [
174+
{
175+
id: 'custom-agent',
176+
displayName: 'Custom Agent',
177+
spawnerPrompt: 'A custom agent',
178+
model: 'anthropic/claude-4-sonnet-20250522',
179+
outputMode: 'last_message' as const,
180+
includeMessageHistory: false,
181+
inheritParentSystemPrompt: false,
182+
mcpServers: {},
183+
toolNames: [],
184+
spawnableAgents: [],
185+
inputSchema: {},
186+
systemPrompt: 'Custom system prompt',
187+
instructionsPrompt: '',
188+
stepPrompt: '',
189+
},
190+
]
191+
192+
const sessionState = await initialSessionState({
193+
cwd: '/test-project',
194+
projectFiles,
195+
agentDefinitions,
196+
fs: mockFs,
197+
logger: mockLogger,
198+
})
199+
200+
expect(sessionState.fileContext.agentTemplates).toBeDefined()
201+
expect(sessionState.fileContext.agentTemplates['custom-agent']).toBeDefined()
202+
expect(sessionState.fileContext.agentTemplates['custom-agent'].displayName).toBe(
203+
'Custom Agent',
204+
)
205+
})
206+
207+
test('includes custom tool definitions', async () => {
208+
const projectFiles = {
209+
'src/index.ts': 'console.log("Hello world");',
210+
}
211+
212+
const inputSchema = z.object({ input: z.string() })
213+
const customToolDefinitions = [
214+
{
215+
toolName: 'custom_tool',
216+
zodSchema: inputSchema,
217+
inputJsonSchema: {
218+
type: 'object' as const,
219+
properties: {
220+
input: { type: 'string' as const },
221+
},
222+
},
223+
description: 'A custom tool',
224+
endsAgentStep: false,
225+
exampleInputs: [],
226+
execute: async (input: any) => [],
227+
},
228+
]
229+
230+
const sessionState = await initialSessionState({
231+
cwd: '/test-project',
232+
projectFiles,
233+
customToolDefinitions,
234+
fs: mockFs,
235+
logger: mockLogger,
236+
})
237+
238+
expect(sessionState.fileContext.customToolDefinitions).toBeDefined()
239+
expect(sessionState.fileContext.customToolDefinitions['custom_tool']).toBeDefined()
240+
expect(sessionState.fileContext.customToolDefinitions['custom_tool'].description).toBe(
241+
'A custom tool',
242+
)
243+
})
244+
245+
test('populates system info correctly', async () => {
246+
const projectFiles = {
247+
'src/index.ts': 'console.log("Hello world");',
248+
}
249+
250+
const sessionState = await initialSessionState({
251+
cwd: '/test-project',
252+
projectFiles,
253+
fs: mockFs,
254+
logger: mockLogger,
255+
})
256+
257+
expect(sessionState.fileContext.systemInfo).toBeDefined()
258+
expect(sessionState.fileContext.systemInfo.platform).toBe(process.platform)
259+
expect(sessionState.fileContext.systemInfo.shell).toBeDefined()
260+
expect(sessionState.fileContext.systemInfo.nodeVersion).toBe(process.version)
261+
expect(sessionState.fileContext.systemInfo.cpus).toBeGreaterThan(0)
262+
})
263+
264+
test('initializes empty agent state correctly', async () => {
265+
const projectFiles = {
266+
'src/index.ts': 'console.log("Hello world");',
267+
}
268+
269+
const sessionState = await initialSessionState({
270+
cwd: '/test-project',
271+
projectFiles,
272+
fs: mockFs,
273+
logger: mockLogger,
274+
})
275+
276+
expect(sessionState.mainAgentState.agentId).toBe('main-agent')
277+
expect(sessionState.mainAgentState.agentType).toBeNull()
278+
expect(sessionState.mainAgentState.agentContext).toEqual({})
279+
expect(sessionState.mainAgentState.ancestorRunIds).toEqual([])
280+
expect(sessionState.mainAgentState.subagents).toEqual([])
281+
expect(sessionState.mainAgentState.childRunIds).toEqual([])
282+
expect(sessionState.mainAgentState.messageHistory).toEqual([])
283+
expect(sessionState.mainAgentState.creditsUsed).toBe(0)
284+
expect(sessionState.mainAgentState.directCreditsUsed).toBe(0)
285+
expect(sessionState.mainAgentState.output).toBeUndefined()
286+
expect(sessionState.mainAgentState.parentId).toBeUndefined()
287+
})
288+
})

sdk/src/run-state.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -196,12 +196,6 @@ export async function initialSessionState(
196196
if (!customToolDefinitions) {
197197
customToolDefinitions = []
198198
}
199-
if (!projectFiles) {
200-
projectFiles = {}
201-
}
202-
if (!knowledgeFiles) {
203-
knowledgeFiles = {}
204-
}
205199
if (!fs) {
206200
fs = (require('fs') as typeof fsType).promises
207201
}
@@ -218,11 +212,9 @@ export async function initialSessionState(
218212
if (projectFiles === undefined && cwd) {
219213
projectFiles = await discoverProjectFiles({ cwd, fs, logger })
220214
}
221-
logger.info({ projectFiles }, 'asdf')
222215
if (knowledgeFiles === undefined) {
223216
knowledgeFiles = projectFiles ? deriveKnowledgeFiles(projectFiles) : {}
224217
}
225-
logger.info({ knowledgeFiles }, 'asdf')
226218

227219
const processedAgentTemplates = processAgentDefinitions(agentDefinitions)
228220
const processedCustomToolDefinitions = processCustomToolDefinitions(

sdk/src/run.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ export async function run({
349349
projectFiles,
350350
maxAgentSteps,
351351
fs,
352+
logger,
352353
})
353354
}
354355

0 commit comments

Comments
 (0)