Skip to content

Commit ba1d4d0

Browse files
committed
add fetchAgentFromDatabase to AgentRuntimeDeps
1 parent a3a7566 commit ba1d4d0

File tree

22 files changed

+526
-539
lines changed

22 files changed

+526
-539
lines changed

backend/src/__tests__/agent-registry.test.ts

Lines changed: 100 additions & 261 deletions
Large diffs are not rendered by default.

backend/src/api/__tests__/validate-agent-name.test.ts

Lines changed: 84 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
import { AGENT_PERSONAS } from '@codebuff/common/constants/agents'
2-
import {
3-
describe,
4-
it,
5-
expect,
6-
beforeEach,
7-
afterEach,
8-
spyOn,
9-
mock,
10-
} from 'bun:test'
11-
12-
import * as agentRegistry from '../../templates/agent-registry'
13-
import { validateAgentNameHandler } from '../agents'
2+
import { TEST_AGENT_RUNTIME_IMPL } from '@codebuff/common/testing/impl/agent-runtime'
3+
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test'
144

5+
import { validateAgentNameHandlerHelper } from '../validate-agent-name'
6+
7+
import type { AgentRuntimeDeps } from '@codebuff/common/types/contracts/agent-runtime'
8+
import type { FetchAgentFromDatabaseFn } from '@codebuff/common/types/contracts/database'
159
import type {
1610
Request as ExpressRequest,
1711
Response as ExpressResponse,
1812
NextFunction,
1913
} from 'express'
2014

15+
let agentRuntimeImpl: AgentRuntimeDeps
16+
2117
function createMockReq(query: Record<string, any>): Partial<ExpressRequest> {
2218
return {
2319
query,
@@ -43,11 +39,24 @@ function createMockRes() {
4339

4440
const noopNext: NextFunction = () => {}
4541

42+
function mockFetchAgentFromDatabase(
43+
returnValue: ReturnType<FetchAgentFromDatabaseFn>,
44+
) {
45+
const spy = mock((input) => {
46+
return returnValue
47+
})
48+
agentRuntimeImpl = {
49+
...agentRuntimeImpl,
50+
fetchAgentFromDatabase: spy,
51+
}
52+
return spy
53+
}
54+
4655
describe('validateAgentNameHandler', () => {
4756
const builtinAgentId = Object.keys(AGENT_PERSONAS)[0] || 'file-picker'
4857

4958
beforeEach(() => {
50-
mock.restore()
59+
agentRuntimeImpl = { ...TEST_AGENT_RUNTIME_IMPL }
5160
})
5261

5362
afterEach(() => {
@@ -58,7 +67,12 @@ describe('validateAgentNameHandler', () => {
5867
const req = createMockReq({ agentId: builtinAgentId })
5968
const res = createMockRes()
6069

61-
await validateAgentNameHandler(req as any, res as any, noopNext)
70+
await validateAgentNameHandlerHelper({
71+
...agentRuntimeImpl,
72+
req: req as any,
73+
res: res as any,
74+
next: noopNext,
75+
})
6276

6377
expect(res.status).toHaveBeenCalledWith(200)
6478
expect(res.json).toHaveBeenCalled()
@@ -70,17 +84,28 @@ describe('validateAgentNameHandler', () => {
7084
it('returns valid=true for published agent ids (publisher/name)', async () => {
7185
const agentId = 'codebuff/file-explorer'
7286

73-
const spy = spyOn(agentRegistry, 'getAgentTemplate')
74-
spy.mockResolvedValueOnce({ id: 'codebuff/file-explorer@0.0.1' } as any)
87+
const spy = mockFetchAgentFromDatabase(
88+
Promise.resolve({
89+
id: 'codebuff/file-explorer@0.0.1',
90+
} as any),
91+
)
7592

7693
const req = createMockReq({ agentId })
7794
const res = createMockRes()
7895

79-
await validateAgentNameHandler(req as any, res as any, noopNext)
96+
await validateAgentNameHandlerHelper({
97+
...agentRuntimeImpl,
98+
req: req as any,
99+
res: res as any,
100+
next: noopNext,
101+
})
80102

81103
expect(spy).toHaveBeenCalledWith({
82-
agentId,
83-
localAgentTemplates: {},
104+
parsedAgentId: {
105+
publisherId: 'codebuff',
106+
agentId: 'file-explorer',
107+
version: undefined,
108+
},
84109
logger: expect.anything(),
85110
})
86111
expect(res.status).toHaveBeenCalledWith(200)
@@ -92,17 +117,28 @@ describe('validateAgentNameHandler', () => {
92117
it('returns valid=true for versioned published agent ids (publisher/name@version)', async () => {
93118
const agentId = 'codebuff/file-explorer@0.0.1'
94119

95-
const spy = spyOn(agentRegistry, 'getAgentTemplate')
96-
spy.mockResolvedValueOnce({ id: agentId } as any)
120+
const spy = mockFetchAgentFromDatabase(
121+
Promise.resolve({
122+
id: agentId,
123+
} as any),
124+
)
97125

98126
const req = createMockReq({ agentId })
99127
const res = createMockRes()
100128

101-
await validateAgentNameHandler(req as any, res as any, noopNext)
129+
await validateAgentNameHandlerHelper({
130+
...agentRuntimeImpl,
131+
req: req as any,
132+
res: res as any,
133+
next: noopNext,
134+
})
102135

103136
expect(spy).toHaveBeenCalledWith({
104-
agentId,
105-
localAgentTemplates: {},
137+
parsedAgentId: {
138+
publisherId: 'codebuff',
139+
agentId: 'file-explorer',
140+
version: '0.0.1',
141+
},
106142
logger: expect.anything(),
107143
})
108144
expect(res.status).toHaveBeenCalledWith(200)
@@ -114,17 +150,24 @@ describe('validateAgentNameHandler', () => {
114150
it('returns valid=false for unknown agents', async () => {
115151
const agentId = 'someorg/not-a-real-agent'
116152

117-
const spy = spyOn(agentRegistry, 'getAgentTemplate')
118-
spy.mockResolvedValueOnce(null)
153+
const spy = mockFetchAgentFromDatabase(Promise.resolve(null))
119154

120155
const req = createMockReq({ agentId })
121156
const res = createMockRes()
122157

123-
await validateAgentNameHandler(req as any, res as any, noopNext)
158+
await validateAgentNameHandlerHelper({
159+
...agentRuntimeImpl,
160+
req: req as any,
161+
res: res as any,
162+
next: noopNext,
163+
})
124164

125165
expect(spy).toHaveBeenCalledWith({
126-
agentId,
127-
localAgentTemplates: {},
166+
parsedAgentId: {
167+
publisherId: 'someorg',
168+
agentId: 'not-a-real-agent',
169+
version: undefined,
170+
},
128171
logger: expect.anything(),
129172
})
130173
expect(res.status).toHaveBeenCalledWith(200)
@@ -135,7 +178,12 @@ describe('validateAgentNameHandler', () => {
135178
const req = createMockReq({})
136179
const res = createMockRes()
137180

138-
await validateAgentNameHandler(req as any, res as any, noopNext)
181+
await validateAgentNameHandlerHelper({
182+
...agentRuntimeImpl,
183+
req: req as any,
184+
res: res as any,
185+
next: noopNext,
186+
})
139187

140188
// Handler normalizes zod errors to 400
141189
expect(res.status).toHaveBeenCalledWith(400)
@@ -147,7 +195,12 @@ describe('validateAgentNameHandler', () => {
147195
const req = { query: { agentId: 'test' }, headers: {} } as any
148196
const res = createMockRes()
149197

150-
await validateAgentNameHandler(req as any, res as any, noopNext)
198+
await validateAgentNameHandlerHelper({
199+
...agentRuntimeImpl,
200+
req: req as any,
201+
res: res as any,
202+
next: noopNext,
203+
})
151204

152205
expect(res.status).toHaveBeenCalledWith(403)
153206
expect(res.jsonPayload.valid).toBe(false)

backend/src/api/agents.ts

Lines changed: 8 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,22 @@
1-
import { AGENT_PERSONAS } from '@codebuff/common/constants/agents'
2-
import { z } from 'zod/v4'
3-
4-
import { getAgentTemplate } from '../templates/agent-registry'
5-
import { extractAuthTokenFromHeader } from '../util/auth-helpers'
6-
import { logger } from '../util/logger'
1+
import { validateAgentNameHandlerHelper } from './validate-agent-name'
2+
import { BACKEND_AGENT_RUNTIME_IMPL } from '../impl/agent-runtime'
73

84
import type {
95
Request as ExpressRequest,
106
Response as ExpressResponse,
117
NextFunction,
128
} from 'express'
139

14-
// Add short-lived cache for positive validations
15-
const AGENT_VALIDATION_CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
16-
17-
type CacheEntry = {
18-
result: {
19-
valid: true
20-
source?: string
21-
normalizedId?: string
22-
displayName?: string
23-
}
24-
expiresAt: number
25-
}
26-
27-
const agentValidationCache = new Map<string, CacheEntry>()
28-
29-
// Simple request schema
30-
const validateAgentRequestSchema = z.object({
31-
agentId: z.string().min(1),
32-
})
33-
3410
// GET /api/agents/validate-name
3511
export async function validateAgentNameHandler(
3612
req: ExpressRequest,
3713
res: ExpressResponse,
3814
next: NextFunction,
3915
): Promise<void | ExpressResponse> {
40-
try {
41-
// Check for x-codebuff-api-key header for authentication
42-
const apiKey = extractAuthTokenFromHeader(req)
43-
if (!apiKey) {
44-
return res.status(403).json({
45-
valid: false,
46-
message: 'API key required',
47-
})
48-
}
49-
50-
// Parse from query instead (GET)
51-
const { agentId } = validateAgentRequestSchema.parse({
52-
agentId: String((req.query as any)?.agentId ?? ''),
53-
})
54-
55-
// Check cache (positive results only)
56-
const cached = agentValidationCache.get(agentId)
57-
if (cached && cached.expiresAt > Date.now()) {
58-
return res.status(200).json({ ...cached.result, cached: true })
59-
} else if (cached) {
60-
agentValidationCache.delete(agentId)
61-
}
62-
63-
// Check built-in agents first
64-
const persona = AGENT_PERSONAS[agentId as keyof typeof AGENT_PERSONAS]
65-
if (persona) {
66-
const result = {
67-
valid: true as const,
68-
source: 'builtin',
69-
normalizedId: agentId,
70-
displayName: persona.displayName,
71-
}
72-
agentValidationCache.set(agentId, {
73-
result,
74-
expiresAt: Date.now() + AGENT_VALIDATION_CACHE_TTL_MS,
75-
})
76-
return res.status(200).json(result)
77-
}
78-
79-
// Check published agents (database)
80-
const found = await getAgentTemplate({ agentId, localAgentTemplates: {}, logger })
81-
if (found) {
82-
const result = {
83-
valid: true as const,
84-
source: 'published',
85-
normalizedId: found.id,
86-
displayName: found.displayName,
87-
}
88-
agentValidationCache.set(agentId, {
89-
result,
90-
expiresAt: Date.now() + AGENT_VALIDATION_CACHE_TTL_MS,
91-
})
92-
return res.status(200).json(result)
93-
}
94-
95-
return res.status(200).json({ valid: false })
96-
} catch (error) {
97-
logger.error(
98-
{ error: error instanceof Error ? error.message : String(error) },
99-
'Error validating agent name',
100-
)
101-
if (error instanceof z.ZodError) {
102-
return res.status(400).json({
103-
valid: false,
104-
message: 'Invalid request',
105-
issues: error.issues,
106-
})
107-
}
108-
next(error)
109-
return
110-
}
16+
return validateAgentNameHandlerHelper({
17+
...BACKEND_AGENT_RUNTIME_IMPL,
18+
req,
19+
res,
20+
next,
21+
})
11122
}

0 commit comments

Comments
 (0)