Skip to content

Commit 082329b

Browse files
committed
add agent steps endpoint
1 parent 19aafb9 commit 082329b

File tree

8 files changed

+476
-10
lines changed

8 files changed

+476
-10
lines changed

web/.prettierrc.js

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import { TEST_USER_ID } from '@codebuff/common/old-constants'
2+
import { beforeEach, describe, expect, test } from 'bun:test'
3+
import { NextRequest } from 'next/server'
4+
5+
import { agentRunsStepsPost } from '../steps'
6+
7+
import type { TrackEventFn } from '@codebuff/common/types/contracts/analytics'
8+
import type { GetUserInfoFromApiKeyFn } from '@codebuff/common/types/contracts/database'
9+
import type { Logger } from '@codebuff/common/types/contracts/logger'
10+
11+
12+
describe('agentRunsStepsPost', () => {
13+
let mockGetUserInfoFromApiKey: GetUserInfoFromApiKeyFn
14+
let mockLogger: Logger
15+
let mockTrackEvent: TrackEventFn
16+
let mockDb: any
17+
18+
beforeEach(() => {
19+
mockGetUserInfoFromApiKey = async ({ apiKey, fields }) => {
20+
if (apiKey === 'valid-key') {
21+
return Object.fromEntries(
22+
fields.map((field) => [field, field === 'id' ? 'user-123' : undefined])
23+
) as any
24+
}
25+
if (apiKey === 'test-key') {
26+
return Object.fromEntries(
27+
fields.map((field) => [field, field === 'id' ? TEST_USER_ID : undefined])
28+
) as any
29+
}
30+
return null
31+
}
32+
33+
mockLogger = {
34+
error: () => {},
35+
warn: () => {},
36+
info: () => {},
37+
debug: () => {},
38+
}
39+
40+
mockTrackEvent = () => {}
41+
42+
// Default mock DB with successful operations
43+
mockDb = {
44+
select: () => ({
45+
from: () => ({
46+
where: () => ({
47+
limit: () => [{ user_id: 'user-123' }],
48+
}),
49+
}),
50+
}),
51+
insert: () => ({
52+
values: async () => {},
53+
}),
54+
}
55+
})
56+
57+
test('returns 401 when no API key provided', async () => {
58+
const req = new NextRequest('http://localhost/api/v1/agent-runs/run-123/steps', {
59+
method: 'POST',
60+
body: JSON.stringify({ stepNumber: 1 }),
61+
})
62+
63+
const response = await agentRunsStepsPost({
64+
req,
65+
runId: 'run-123',
66+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
67+
logger: mockLogger,
68+
trackEvent: mockTrackEvent,
69+
db: mockDb,
70+
})
71+
72+
expect(response.status).toBe(401)
73+
const json = await response.json()
74+
expect(json.error).toBe('Missing or invalid Authorization header')
75+
})
76+
77+
test('returns 404 when API key is invalid', async () => {
78+
const req = new NextRequest('http://localhost/api/v1/agent-runs/run-123/steps', {
79+
method: 'POST',
80+
headers: { Authorization: 'Bearer invalid-key' },
81+
body: JSON.stringify({ stepNumber: 1 }),
82+
})
83+
84+
const response = await agentRunsStepsPost({
85+
req,
86+
runId: 'run-123',
87+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
88+
logger: mockLogger,
89+
trackEvent: mockTrackEvent,
90+
db: mockDb,
91+
})
92+
93+
expect(response.status).toBe(404)
94+
const json = await response.json()
95+
expect(json.error).toBe('Invalid API key or user not found')
96+
})
97+
98+
test('returns 400 when request body is invalid JSON', async () => {
99+
const req = new NextRequest('http://localhost/api/v1/agent-runs/run-123/steps', {
100+
method: 'POST',
101+
headers: { Authorization: 'Bearer valid-key' },
102+
body: 'invalid json',
103+
})
104+
105+
const response = await agentRunsStepsPost({
106+
req,
107+
runId: 'run-123',
108+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
109+
logger: mockLogger,
110+
trackEvent: mockTrackEvent,
111+
db: mockDb,
112+
})
113+
114+
expect(response.status).toBe(400)
115+
const json = await response.json()
116+
expect(json.error).toBe('Invalid JSON in request body')
117+
})
118+
119+
test('returns 400 when schema validation fails', async () => {
120+
const req = new NextRequest('http://localhost/api/v1/agent-runs/run-123/steps', {
121+
method: 'POST',
122+
headers: { Authorization: 'Bearer valid-key' },
123+
body: JSON.stringify({ stepNumber: -1 }), // Invalid: negative
124+
})
125+
126+
const response = await agentRunsStepsPost({
127+
req,
128+
runId: 'run-123',
129+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
130+
logger: mockLogger,
131+
trackEvent: mockTrackEvent,
132+
db: mockDb,
133+
})
134+
135+
expect(response.status).toBe(400)
136+
const json = await response.json()
137+
expect(json.error).toBe('Invalid request body')
138+
})
139+
140+
test('returns 404 when agent run does not exist', async () => {
141+
const dbWithNoRun = {
142+
...mockDb,
143+
select: () => ({
144+
from: () => ({
145+
where: () => ({
146+
limit: () => [], // Empty array = not found
147+
}),
148+
}),
149+
}),
150+
} as any
151+
152+
const req = new NextRequest('http://localhost/api/v1/agent-runs/run-123/steps', {
153+
method: 'POST',
154+
headers: { Authorization: 'Bearer valid-key' },
155+
body: JSON.stringify({ stepNumber: 1 }),
156+
})
157+
158+
const response = await agentRunsStepsPost({
159+
req,
160+
runId: 'run-123',
161+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
162+
logger: mockLogger,
163+
trackEvent: mockTrackEvent,
164+
db: dbWithNoRun,
165+
})
166+
167+
expect(response.status).toBe(404)
168+
const json = await response.json()
169+
expect(json.error).toBe('Agent run not found')
170+
})
171+
172+
test('returns 403 when run belongs to different user', async () => {
173+
const dbWithDifferentUser = {
174+
...mockDb,
175+
select: () => ({
176+
from: () => ({
177+
where: () => ({
178+
limit: () => [{ user_id: 'other-user' }],
179+
}),
180+
}),
181+
}),
182+
} as any
183+
184+
const req = new NextRequest('http://localhost/api/v1/agent-runs/run-123/steps', {
185+
method: 'POST',
186+
headers: { Authorization: 'Bearer valid-key' },
187+
body: JSON.stringify({ stepNumber: 1 }),
188+
})
189+
190+
const response = await agentRunsStepsPost({
191+
req,
192+
runId: 'run-123',
193+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
194+
logger: mockLogger,
195+
trackEvent: mockTrackEvent,
196+
db: dbWithDifferentUser,
197+
})
198+
199+
expect(response.status).toBe(403)
200+
const json = await response.json()
201+
expect(json.error).toBe('Unauthorized to add steps to this run')
202+
})
203+
204+
test('returns test step ID for test user', async () => {
205+
const req = new NextRequest('http://localhost/api/v1/agent-runs/run-123/steps', {
206+
method: 'POST',
207+
headers: { Authorization: 'Bearer test-key' },
208+
body: JSON.stringify({ stepNumber: 1 }),
209+
})
210+
211+
const response = await agentRunsStepsPost({
212+
req,
213+
runId: 'run-123',
214+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
215+
logger: mockLogger,
216+
trackEvent: mockTrackEvent,
217+
db: mockDb,
218+
})
219+
220+
expect(response.status).toBe(200)
221+
const json = await response.json()
222+
expect(json.stepId).toBe('test-step-id')
223+
})
224+
225+
test('successfully adds agent step', async () => {
226+
const req = new NextRequest('http://localhost/api/v1/agent-runs/run-123/steps', {
227+
method: 'POST',
228+
headers: { Authorization: 'Bearer valid-key' },
229+
body: JSON.stringify({
230+
stepNumber: 1,
231+
credits: 100,
232+
childRunIds: ['child-1', 'child-2'],
233+
messageId: 'msg-123',
234+
status: 'completed',
235+
}),
236+
})
237+
238+
const response = await agentRunsStepsPost({
239+
req,
240+
runId: 'run-123',
241+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
242+
logger: mockLogger,
243+
trackEvent: mockTrackEvent,
244+
db: mockDb,
245+
})
246+
247+
expect(response.status).toBe(200)
248+
const json = await response.json()
249+
expect(json.stepId).toBeTruthy()
250+
expect(typeof json.stepId).toBe('string')
251+
})
252+
253+
test('handles database errors gracefully', async () => {
254+
const dbWithError = {
255+
...mockDb,
256+
select: () => ({
257+
from: () => ({
258+
where: () => ({
259+
limit: () => [{ user_id: 'user-123' }],
260+
}),
261+
}),
262+
}),
263+
insert: () => ({
264+
values: async () => {
265+
throw new Error('DB error')
266+
},
267+
}),
268+
} as any
269+
270+
const req = new NextRequest('http://localhost/api/v1/agent-runs/run-123/steps', {
271+
method: 'POST',
272+
headers: { Authorization: 'Bearer valid-key' },
273+
body: JSON.stringify({ stepNumber: 1 }),
274+
})
275+
276+
const response = await agentRunsStepsPost({
277+
req,
278+
runId: 'run-123',
279+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
280+
logger: mockLogger,
281+
trackEvent: mockTrackEvent,
282+
db: dbWithError,
283+
})
284+
285+
expect(response.status).toBe(500)
286+
const json = await response.json()
287+
expect(json.error).toBe('Failed to add agent step')
288+
})
289+
})

0 commit comments

Comments
 (0)