mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-16 01:15:26 -05:00
Migrate knowledge unit tests
This commit is contained in:
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
|
||||
208
apps/sim/tools/knowledge/knowledge.test.ts
Normal file
208
apps/sim/tools/knowledge/knowledge.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user