Migrate knowledge unit tests

This commit is contained in:
Theodore Li
2026-02-15 21:58:16 -08:00
parent 68da290b6f
commit ce02a309a0
7 changed files with 229 additions and 174 deletions

View File

@@ -148,165 +148,4 @@ describe('GenericBlockHandler', () => {
)
})
describe('Knowledge block cost tracking', () => {
beforeEach(() => {
// Set up knowledge block mock
mockBlock = {
...mockBlock,
config: { tool: 'knowledge_search', params: {} },
}
mockTool = {
...mockTool,
id: 'knowledge_search',
name: 'Knowledge Search',
}
mockGetTool.mockImplementation((toolId) => {
if (toolId === 'knowledge_search') {
return mockTool
}
return undefined
})
})
it.concurrent(
'should pass through cost information from knowledge tools unchanged',
async () => {
const inputs = { query: 'test query' }
// Tool's transformResponse already restructures cost, so executeTool returns restructured data
const mockToolResponse = {
success: true,
output: {
results: [],
query: 'test query',
totalResults: 0,
cost: {
input: 0.00001042,
output: 0,
total: 0.00001042,
},
tokens: {
input: 521,
output: 0,
total: 521,
},
model: 'text-embedding-3-small',
},
}
mockExecuteTool.mockResolvedValue(mockToolResponse)
const result = await handler.execute(mockContext, mockBlock, inputs)
// Generic handler passes through output unchanged
expect(result).toEqual({
results: [],
query: 'test query',
totalResults: 0,
cost: {
input: 0.00001042,
output: 0,
total: 0.00001042,
},
tokens: {
input: 521,
output: 0,
total: 521,
},
model: 'text-embedding-3-small',
})
}
)
it.concurrent('should pass through knowledge_upload_chunk output unchanged', async () => {
// Update to upload_chunk tool
mockBlock.config.tool = 'knowledge_upload_chunk'
mockTool.id = 'knowledge_upload_chunk'
mockTool.name = 'Knowledge Upload Chunk'
mockGetTool.mockImplementation((toolId) => {
if (toolId === 'knowledge_upload_chunk') {
return mockTool
}
return undefined
})
const inputs = { content: 'test content' }
// Tool's transformResponse already restructures cost
const mockToolResponse = {
success: true,
output: {
data: {
id: 'chunk-123',
content: 'test content',
chunkIndex: 0,
},
message: 'Successfully uploaded chunk',
documentId: 'doc-123',
cost: {
input: 0.00000521,
output: 0,
total: 0.00000521,
},
tokens: {
input: 260,
output: 0,
total: 260,
},
model: 'text-embedding-3-small',
},
}
mockExecuteTool.mockResolvedValue(mockToolResponse)
const result = await handler.execute(mockContext, mockBlock, inputs)
// Generic handler passes through output unchanged
expect(result).toEqual({
data: {
id: 'chunk-123',
content: 'test content',
chunkIndex: 0,
},
message: 'Successfully uploaded chunk',
documentId: 'doc-123',
cost: {
input: 0.00000521,
output: 0,
total: 0.00000521,
},
tokens: {
input: 260,
output: 0,
total: 260,
},
model: 'text-embedding-3-small',
})
})
it('should pass through output unchanged for knowledge tools without cost info', async () => {
const inputs = { query: 'test query' }
const mockToolResponse = {
success: true,
output: {
results: [],
query: 'test query',
totalResults: 0,
// No cost information
},
}
mockExecuteTool.mockResolvedValue(mockToolResponse)
const result = await handler.execute(mockContext, mockBlock, inputs)
// Should return original output unchanged
expect(result).toEqual({
results: [],
query: 'test query',
totalResults: 0,
})
})
})
})

View File

@@ -38,8 +38,9 @@ export const answerTool: ToolConfig<ExaAnswerParams, ExaAnswerResponse> = {
type: 'custom',
getCost: (_params, output) => {
// Use _costDollars from Exa API response (internal field, stripped from final output)
if (output._costDollars?.total) {
return { cost: output._costDollars.total, metadata: { costDollars: output._costDollars } }
const costDollars = output._costDollars as { total?: number } | undefined
if (costDollars?.total) {
return { cost: costDollars.total, metadata: { costDollars } }
}
// Fallback: $5/1000 requests
logger.warn('Exa answer response missing costDollars, using fallback pricing')

View File

@@ -87,12 +87,14 @@ export const findSimilarLinksTool: ToolConfig<
type: 'custom',
getCost: (_params, output) => {
// Use _costDollars from Exa API response (internal field, stripped from final output)
if (output._costDollars?.total) {
return { cost: output._costDollars.total, metadata: { costDollars: output._costDollars } }
const costDollars = output._costDollars as { total?: number } | undefined
if (costDollars?.total) {
return { cost: costDollars.total, metadata: { costDollars } }
}
// Fallback: $5/1000 (1-25 results) or $25/1000 (26-100 results)
logger.warn('Exa find_similar_links response missing costDollars, using fallback pricing')
const resultCount = output.similarLinks?.length || 0
const similarLinks = output.similarLinks as unknown[] | undefined
const resultCount = similarLinks?.length || 0
return resultCount <= 25 ? 0.005 : 0.025
},
},

View File

@@ -72,12 +72,14 @@ export const getContentsTool: ToolConfig<ExaGetContentsParams, ExaGetContentsRes
type: 'custom',
getCost: (_params, output) => {
// Use _costDollars from Exa API response (internal field, stripped from final output)
if (output._costDollars?.total) {
return { cost: output._costDollars.total, metadata: { costDollars: output._costDollars } }
const costDollars = output._costDollars as { total?: number } | undefined
if (costDollars?.total) {
return { cost: costDollars.total, metadata: { costDollars } }
}
// Fallback: $1/1000 pages
logger.warn('Exa get_contents response missing costDollars, using fallback pricing')
return (output.results?.length || 0) * 0.001
const results = output.results as unknown[] | undefined
return (results?.length || 0) * 0.001
},
},
},

View File

@@ -42,8 +42,9 @@ export const researchTool: ToolConfig<ExaResearchParams, ExaResearchResponse> =
type: 'custom',
getCost: (params, output) => {
// Use _costDollars from Exa API response (internal field, stripped from final output)
if (output._costDollars?.total) {
return { cost: output._costDollars.total, metadata: { costDollars: output._costDollars } }
const costDollars = output._costDollars as { total?: number } | undefined
if (costDollars?.total) {
return { cost: costDollars.total, metadata: { costDollars } }
}
// Fallback to estimate if cost not available

View File

@@ -97,8 +97,9 @@ export const searchTool: ToolConfig<ExaSearchParams, ExaSearchResponse> = {
type: 'custom',
getCost: (params, output) => {
// Use _costDollars from Exa API response (internal field, stripped from final output)
if (output._costDollars?.total) {
return { cost: output._costDollars.total, metadata: { costDollars: output._costDollars } }
const costDollars = output._costDollars as { total?: number } | undefined
if (costDollars?.total) {
return { cost: costDollars.total, metadata: { costDollars } }
}
// Fallback: estimate based on search type and result count
@@ -107,7 +108,8 @@ export const searchTool: ToolConfig<ExaSearchParams, ExaSearchResponse> = {
if (isDeepSearch) {
return 0.015
}
const resultCount = output.results?.length || 0
const results = output.results as unknown[] | undefined
const resultCount = results?.length || 0
return resultCount <= 25 ? 0.005 : 0.025
},
},

View File

@@ -0,0 +1,208 @@
/**
* @vitest-environment node
*
* Knowledge Tools Unit Tests
*
* Tests for knowledge_search and knowledge_upload_chunk tools,
* specifically the cost restructuring in transformResponse.
*/
import { describe, expect, it } from 'vitest'
import { knowledgeSearchTool } from '@/tools/knowledge/search'
import { knowledgeUploadChunkTool } from '@/tools/knowledge/upload_chunk'
/**
* Creates a mock Response object for testing transformResponse
*/
function createMockResponse(data: unknown): Response {
return {
json: async () => data,
ok: true,
status: 200,
} as Response
}
describe('Knowledge Tools', () => {
describe('knowledgeSearchTool', () => {
describe('transformResponse', () => {
it('should restructure cost information for logging', async () => {
const apiResponse = {
data: {
results: [{ content: 'test result', similarity: 0.95 }],
query: 'test query',
totalResults: 1,
cost: {
input: 0.00001042,
output: 0,
total: 0.00001042,
tokens: {
prompt: 521,
completion: 0,
total: 521,
},
model: 'text-embedding-3-small',
pricing: {
input: 0.02,
output: 0,
updatedAt: '2025-07-10',
},
},
},
}
const result = await knowledgeSearchTool.transformResponse!(
createMockResponse(apiResponse)
)
expect(result.success).toBe(true)
expect(result.output).toEqual({
results: [{ content: 'test result', similarity: 0.95 }],
query: 'test query',
totalResults: 1,
cost: {
input: 0.00001042,
output: 0,
total: 0.00001042,
},
tokens: {
prompt: 521,
completion: 0,
total: 521,
},
model: 'text-embedding-3-small',
})
})
it('should handle response without cost information', async () => {
const apiResponse = {
data: {
results: [],
query: 'test query',
totalResults: 0,
},
}
const result = await knowledgeSearchTool.transformResponse!(
createMockResponse(apiResponse)
)
expect(result.success).toBe(true)
expect(result.output).toEqual({
results: [],
query: 'test query',
totalResults: 0,
})
expect(result.output.cost).toBeUndefined()
expect(result.output.tokens).toBeUndefined()
expect(result.output.model).toBeUndefined()
})
it('should handle response with partial cost information', async () => {
const apiResponse = {
data: {
results: [],
query: 'test query',
totalResults: 0,
cost: {
input: 0.001,
output: 0,
total: 0.001,
// No tokens or model
},
},
}
const result = await knowledgeSearchTool.transformResponse!(
createMockResponse(apiResponse)
)
expect(result.success).toBe(true)
expect(result.output.cost).toEqual({
input: 0.001,
output: 0,
total: 0.001,
})
expect(result.output.tokens).toBeUndefined()
expect(result.output.model).toBeUndefined()
})
})
})
describe('knowledgeUploadChunkTool', () => {
describe('transformResponse', () => {
it('should restructure cost information for logging', async () => {
const apiResponse = {
data: {
id: 'chunk-123',
chunkIndex: 0,
content: 'test content',
contentLength: 12,
tokenCount: 3,
enabled: true,
documentId: 'doc-456',
documentName: 'Test Document',
createdAt: '2025-01-01T00:00:00Z',
updatedAt: '2025-01-01T00:00:00Z',
cost: {
input: 0.00000521,
output: 0,
total: 0.00000521,
tokens: {
prompt: 260,
completion: 0,
total: 260,
},
model: 'text-embedding-3-small',
pricing: {
input: 0.02,
output: 0,
updatedAt: '2025-07-10',
},
},
},
}
const result = await knowledgeUploadChunkTool.transformResponse!(
createMockResponse(apiResponse)
)
expect(result.success).toBe(true)
expect(result.output.cost).toEqual({
input: 0.00000521,
output: 0,
total: 0.00000521,
})
expect(result.output.tokens).toEqual({
prompt: 260,
completion: 0,
total: 260,
})
expect(result.output.model).toBe('text-embedding-3-small')
expect(result.output.data.chunkId).toBe('chunk-123')
expect(result.output.documentId).toBe('doc-456')
})
it('should handle response without cost information', async () => {
const apiResponse = {
data: {
id: 'chunk-123',
chunkIndex: 0,
content: 'test content',
documentId: 'doc-456',
documentName: 'Test Document',
},
}
const result = await knowledgeUploadChunkTool.transformResponse!(
createMockResponse(apiResponse)
)
expect(result.success).toBe(true)
expect(result.output.cost).toBeUndefined()
expect(result.output.tokens).toBeUndefined()
expect(result.output.model).toBeUndefined()
expect(result.output.data.chunkId).toBe('chunk-123')
})
})
})
})