mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
Every first-party Python caller and test now names the mount the API actually answers on. Mechanical: URL literals only, and only where the /api/ was ours -- a preceding word character means it was a module path like insights/api/ or a third-party host like slack.com/api/, and those are untouched. Left saying /api/, each for a reason rather than for safety: - products/warehouse_sources/.../sources/** is a catalogue of vendor endpoints - services/llm-gateway deploys separately and is under a code freeze, so moving its two calls would break it during the skew - the mcp_store and Slack/Teams redirect_uris are registered with third parties and must byte-match what was registered - the Wizard and Raycast client-metadata paths are OAuth client_ids - /api/surveys and /api/feature_flag/local_evaluation are SDK aliases the ingress routes to the Rust fleets, held by released SDK versions Test URLconfs moved with the requests that call them, and the Datastore user-selection fixtures now carry the leading slash that request.path has.
146 lines
5.7 KiB
TypeScript
146 lines
5.7 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
TEST_ORG_ID,
|
|
TEST_PROJECT_ID,
|
|
createTestClient,
|
|
createTestContext,
|
|
generateUniqueKey,
|
|
getToolByName,
|
|
parseToolResponse,
|
|
setActiveProjectAndOrg,
|
|
validateEnvironmentVariables,
|
|
} from '@/shared/test-utils'
|
|
import { GENERATED_TOOLS as FF_TOOLS } from '@/tools/generated/feature_flags'
|
|
import type { Context } from '@/tools/types'
|
|
|
|
const BK_FEATURE_FLAG = 'product-business-knowledge'
|
|
|
|
describe('Business knowledge sources', { concurrent: false }, () => {
|
|
let context: Context
|
|
const createdSourceIds: string[] = []
|
|
|
|
beforeAll(async () => {
|
|
validateEnvironmentVariables()
|
|
const client = createTestClient()
|
|
context = createTestContext(client)
|
|
await setActiveProjectAndOrg(context, TEST_PROJECT_ID!, TEST_ORG_ID!)
|
|
|
|
// The BK API is gated behind the product-business-knowledge flag — ensure it's active.
|
|
const listFlags = FF_TOOLS['feature-flag-get-all']!()
|
|
const updateFlag = FF_TOOLS['update-feature-flag']!()
|
|
const createFlag = FF_TOOLS['create-feature-flag']!()
|
|
|
|
const flagsResult = await listFlags.handler(context, { search: BK_FEATURE_FLAG })
|
|
const flags = (flagsResult as any).results ?? []
|
|
const flag = flags.find((f: any) => f.key === BK_FEATURE_FLAG)
|
|
|
|
if (flag && !flag.active) {
|
|
await updateFlag.handler(context, { id: flag.id, active: true })
|
|
} else if (!flag) {
|
|
await createFlag.handler(context, {
|
|
key: BK_FEATURE_FLAG,
|
|
name: 'Business knowledge',
|
|
active: true,
|
|
filters: {
|
|
groups: [{ properties: [], rollout_percentage: 100 }],
|
|
},
|
|
})
|
|
}
|
|
})
|
|
|
|
afterAll(async () => {
|
|
for (const id of createdSourceIds) {
|
|
try {
|
|
await context.api.request({
|
|
method: 'DELETE',
|
|
path: `/v1/projects/${TEST_PROJECT_ID}/business_knowledge/sources/${id}/`,
|
|
})
|
|
} catch (error) {
|
|
console.warn(`Failed to cleanup knowledge source ${id}:`, error)
|
|
}
|
|
}
|
|
})
|
|
|
|
describe('text-create and retrieve', () => {
|
|
const textCreateTool = getToolByName('business-knowledge-sources-text-create')
|
|
const retrieveTool = getToolByName('business-knowledge-sources-retrieve')
|
|
|
|
it('should create a text source and retrieve it by ID', async () => {
|
|
const name = generateUniqueKey('MCP Text Source')
|
|
const result = await textCreateTool.handler(context, {
|
|
name,
|
|
text: 'Insights is an open-source product analytics platform.',
|
|
})
|
|
const source = parseToolResponse(result)
|
|
|
|
expect(source.id).toBeTruthy()
|
|
expect(source.name).toBe(name)
|
|
expect(source.source_type).toBe('text')
|
|
createdSourceIds.push(source.id)
|
|
|
|
const retrieved = parseToolResponse(await retrieveTool.handler(context, { id: source.id }))
|
|
expect(retrieved.id).toBe(source.id)
|
|
expect(retrieved.name).toBe(name)
|
|
expect(retrieved.source_type).toBe('text')
|
|
expect(retrieved).not.toHaveProperty('_insightsUrl')
|
|
})
|
|
})
|
|
|
|
describe('url-create', () => {
|
|
const urlCreateTool = getToolByName('business-knowledge-sources-url-create')
|
|
|
|
// Must be a public host: claim_url_source runs the SSRF check (is_url_allowed),
|
|
// which blocks localhost/private hosts. The actual fetch is backgrounded and not
|
|
// awaited here, so the URL only needs to pass SSRF, not return real content.
|
|
const PUBLIC_TEST_URL = 'https://example.com/'
|
|
|
|
// A URL source is claimed in PROCESSING and ingested in the background; the
|
|
// backend enforces a single PROCESSING source per team, so a second concurrent
|
|
// create would 409. We therefore exercise dispatch + refresh_interval in one
|
|
// create rather than two.
|
|
it('should dispatch source_type=url and persist refresh_interval', async () => {
|
|
const name = generateUniqueKey('MCP URL Source')
|
|
const result = await urlCreateTool.handler(context, {
|
|
name,
|
|
url: PUBLIC_TEST_URL,
|
|
refresh_interval: '24h',
|
|
})
|
|
const source = parseToolResponse(result)
|
|
|
|
expect(source.id).toBeTruthy()
|
|
expect(source.name).toBe(name)
|
|
expect(source.source_type).toBe('url')
|
|
expect(source.refresh_interval).toBe('24h')
|
|
createdSourceIds.push(source.id)
|
|
})
|
|
})
|
|
|
|
describe('list', () => {
|
|
const listTool = getToolByName('business-knowledge-sources-list')
|
|
|
|
it('should return sources with filtered response fields', async () => {
|
|
const result = await listTool.handler(context, {})
|
|
const response = parseToolResponse(result)
|
|
|
|
expect(response.results).toBeTruthy()
|
|
expect(Array.isArray(response.results)).toBe(true)
|
|
expect(response).toHaveProperty('_insightsUrl')
|
|
expect(response._insightsUrl).toContain('/business-knowledge')
|
|
|
|
if (response.results.length > 0) {
|
|
const source = response.results[0]
|
|
expect(source).toHaveProperty('id')
|
|
expect(source).toHaveProperty('name')
|
|
expect(source).toHaveProperty('source_type')
|
|
expect(source).toHaveProperty('status')
|
|
|
|
const unexpectedFields = ['documents', 'source_text', 'team']
|
|
for (const field of unexpectedFields) {
|
|
expect(source).not.toHaveProperty(field)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|