chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+413
View File
@@ -0,0 +1,413 @@
/**
* @vitest-environment node
*
* Knowledge Utils Unit Tests
*
* This file contains unit tests for the knowledge base utility functions,
* including access checks, document processing, and embedding generation.
*/
import { createEnvMock } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('drizzle-orm', () => ({
and: (...args: any[]) => args,
eq: (...args: any[]) => args,
isNull: () => true,
sql: (strings: TemplateStringsArray, ...expr: any[]) => ({ strings, expr }),
}))
vi.mock('@/lib/core/config/env', () => createEnvMock({ OPENAI_API_KEY: 'test-key' }))
vi.mock('@/lib/knowledge/documents/utils', () => ({
retryWithExponentialBackoff: (fn: any) => fn(),
}))
vi.mock('@/lib/workspaces/utils', () => ({
getWorkspaceBilledAccountUserId: vi.fn().mockResolvedValue('user1'),
}))
vi.mock('@/lib/knowledge/documents/document-processor', () => ({
processDocument: vi.fn().mockResolvedValue({
chunks: [
{
text: 'alpha',
tokenCount: 1,
metadata: { startIndex: 0, endIndex: 4 },
},
{
text: 'beta',
tokenCount: 1,
metadata: { startIndex: 5, endIndex: 8 },
},
],
metadata: {
filename: 'dummy',
fileSize: 10,
mimeType: 'text/plain',
characterCount: 9,
tokenCount: 3,
chunkCount: 2,
processingMethod: 'file-parser',
},
}),
}))
const dbOps: {
order: string[]
insertRecords: any[][]
updatePayloads: any[]
} = {
order: [],
insertRecords: [],
updatePayloads: [],
}
let kbRows: any[] = []
let docRows: any[] = []
let chunkRows: any[] = []
function resetDatasets() {
kbRows = []
docRows = []
chunkRows = []
}
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
data: [
{ embedding: [0.1, 0.2], index: 0 },
{ embedding: [0.3, 0.4], index: 1 },
],
usage: { prompt_tokens: 2, total_tokens: 2 },
}),
})
)
vi.mock('@sim/db', async () => {
const { schemaMock } = (await import('@sim/testing')) as typeof import('@sim/testing')
const tableNameFor = (table: any) => {
if (table === schemaMock.knowledgeBase) return 'knowledge_base'
if (table === schemaMock.document) return 'document'
if (table === schemaMock.embedding) return 'embedding'
return ''
}
const selectBuilder = {
from(table: any) {
return {
where() {
return {
limit(n: number) {
const tableName = tableNameFor(table)
if (tableName === 'knowledge_base') {
return Promise.resolve(kbRows.slice(0, n))
}
if (tableName === 'document') {
return Promise.resolve(docRows.slice(0, n))
}
if (tableName === 'embedding') {
return Promise.resolve(chunkRows.slice(0, n))
}
return Promise.resolve([])
},
}
},
innerJoin() {
// document × knowledge_base context JOIN — return the first kb and
// doc row merged (covers processDocumentAsync's prefetch).
return {
leftJoin: () => ({
where: () => ({
limit: (n: number) =>
Promise.resolve(
kbRows.length > 0 && docRows.length > 0
? [
{ ...kbRows[0], ...docRows[0], billedAccountUserId: 'billing-user-1' },
].slice(0, n)
: []
),
}),
}),
where: () => ({
limit: (n: number) =>
Promise.resolve(
kbRows.length > 0 && docRows.length > 0
? [{ ...kbRows[0], ...docRows[0] }].slice(0, n)
: []
),
}),
}
},
}
},
}
return {
db: {
select: vi.fn(() => selectBuilder),
update: (table: any) => ({
set: (payload: any) => ({
where: () => {
const tableName = tableNameFor(table)
if (tableName === 'knowledge_base') {
dbOps.order.push('updateKb')
dbOps.updatePayloads.push(payload)
} else if (tableName === 'document') {
if (payload.processingStatus !== 'processing') {
dbOps.order.push('updateDoc')
dbOps.updatePayloads.push(payload)
}
}
return Promise.resolve()
},
}),
}),
delete: () => ({
where: () => Promise.resolve(),
}),
insert: () => ({
values: (records: any) => {
dbOps.order.push('insert')
dbOps.insertRecords.push(records)
return Promise.resolve()
},
}),
transaction: vi.fn(async (fn: any) => {
await fn({
select: () => ({
from: () => ({
innerJoin: () => ({
where: () => ({
limit: () => Promise.resolve([{ id: 'doc1' }]),
}),
}),
where: () => ({
limit: () => Promise.resolve([{}]),
}),
}),
}),
delete: () => ({
where: () => Promise.resolve(),
}),
insert: () => ({
values: (records: any) => {
dbOps.order.push('insert')
dbOps.insertRecords.push(records)
return Promise.resolve()
},
}),
update: () => ({
set: (payload: any) => ({
where: () => {
dbOps.updatePayloads.push(payload)
const label = payload.processingStatus !== undefined ? 'updateDoc' : 'updateKb'
dbOps.order.push(label)
return Promise.resolve()
},
}),
}),
})
}),
},
document: {},
knowledgeBase: {},
embedding: {},
}
})
import { processDocumentAsync } from '@/lib/knowledge/documents/service'
import { generateEmbeddings } from '@/lib/knowledge/embeddings'
import {
checkChunkAccess,
checkDocumentAccess,
checkKnowledgeBaseAccess,
} from '@/app/api/knowledge/utils'
describe('Knowledge Utils', () => {
beforeEach(() => {
dbOps.order.length = 0
dbOps.insertRecords.length = 0
dbOps.updatePayloads.length = 0
resetDatasets()
vi.clearAllMocks()
})
describe('processDocumentAsync', () => {
it.concurrent('should insert embeddings before updating document counters', async () => {
kbRows.push({
id: 'kb1',
userId: 'user1',
workspaceId: 'workspace1',
embeddingModel: 'text-embedding-3-small',
chunkingConfig: { maxSize: 1024, minSize: 1, overlap: 200 },
})
docRows.push({ id: 'doc1', knowledgeBaseId: 'kb1' })
await processDocumentAsync(
'kb1',
'doc1',
{
filename: 'file.txt',
fileUrl: 'https://example.com/file.txt',
fileSize: 10,
mimeType: 'text/plain',
},
{}
)
// Embeddings are inserted first, then the document counter update. A
// usage_log billing insert (recordUsage) may trail after updateDoc and is
// irrelevant to this ordering invariant, so assert position rather than
// exact array equality.
expect(dbOps.order[0]).toBe('insert')
expect(dbOps.order.indexOf('updateDoc')).toBeGreaterThan(0)
expect(dbOps.updatePayloads[0]).toMatchObject({
processingStatus: 'completed',
chunkCount: 2,
})
expect(dbOps.insertRecords[0].length).toBe(2)
})
})
describe('checkKnowledgeBaseAccess', () => {
it.concurrent('should return success for owner', async () => {
kbRows.push({ id: 'kb1', userId: 'user1' })
const result = await checkKnowledgeBaseAccess('kb1', 'user1')
expect(result.hasAccess).toBe(true)
})
it('should return notFound when knowledge base is missing', async () => {
const result = await checkKnowledgeBaseAccess('missing', 'user1')
expect(result.hasAccess).toBe(false)
expect('notFound' in result && result.notFound).toBe(true)
})
})
describe('checkDocumentAccess', () => {
it.concurrent('should return unauthorized when user mismatch', async () => {
kbRows.push({ id: 'kb1', userId: 'owner' })
const result = await checkDocumentAccess('kb1', 'doc1', 'intruder')
expect(result.hasAccess).toBe(false)
if ('reason' in result) {
expect(result.reason).toBe('Unauthorized knowledge base access')
}
})
})
describe('checkChunkAccess', () => {
it.concurrent('should fail when document is not completed', async () => {
kbRows.push({ id: 'kb1', userId: 'user1' })
docRows.push({ id: 'doc1', knowledgeBaseId: 'kb1', processingStatus: 'processing' })
const result = await checkChunkAccess('kb1', 'doc1', 'chunk1', 'user1')
expect(result.hasAccess).toBe(false)
if ('reason' in result) {
expect(result.reason).toContain('Document is not ready')
}
})
it('should return success for valid access', async () => {
kbRows.push({ id: 'kb1', userId: 'user1' })
docRows.push({ id: 'doc1', knowledgeBaseId: 'kb1', processingStatus: 'completed' })
chunkRows.push({ id: 'chunk1', documentId: 'doc1' })
const result = await checkChunkAccess('kb1', 'doc1', 'chunk1', 'user1')
expect(result.hasAccess).toBe(true)
if ('chunk' in result) {
expect(result.chunk.id).toBe('chunk1')
}
})
})
describe('generateEmbeddings', () => {
it.concurrent('should return same length as input', async () => {
const result = await generateEmbeddings(['a', 'b'])
expect(result.embeddings.length).toBe(2)
})
it('should use Azure OpenAI when Azure config is provided', async () => {
const { env } = await import('@/lib/core/config/env')
Object.keys(env).forEach((key) => delete (env as any)[key])
Object.assign(env, {
AZURE_OPENAI_API_KEY: 'test-azure-key',
AZURE_OPENAI_ENDPOINT: 'https://test.openai.azure.com',
AZURE_OPENAI_API_VERSION: '2024-12-01-preview',
KB_OPENAI_MODEL_NAME: 'text-embedding-ada-002',
OPENAI_API_KEY: 'test-openai-key',
})
const fetchSpy = vi.mocked(fetch)
fetchSpy.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: [{ embedding: [0.1, 0.2], index: 0 }],
usage: { prompt_tokens: 1, total_tokens: 1 },
}),
} as any)
await generateEmbeddings(['test text'])
expect(fetchSpy).toHaveBeenCalledWith(
'https://test.openai.azure.com/openai/deployments/text-embedding-ada-002/embeddings?api-version=2024-12-01-preview',
expect.objectContaining({
headers: expect.objectContaining({
'api-key': 'test-azure-key',
}),
})
)
Object.keys(env).forEach((key) => delete (env as any)[key])
})
it('should fallback to OpenAI when no Azure config provided', async () => {
const { env } = await import('@/lib/core/config/env')
Object.keys(env).forEach((key) => delete (env as any)[key])
Object.assign(env, {
OPENAI_API_KEY: 'test-openai-key',
})
const fetchSpy = vi.mocked(fetch)
fetchSpy.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: [{ embedding: [0.1, 0.2], index: 0 }],
usage: { prompt_tokens: 1, total_tokens: 1 },
}),
} as any)
await generateEmbeddings(['test text'])
expect(fetchSpy).toHaveBeenCalledWith(
'https://api.openai.com/v1/embeddings',
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer test-openai-key',
}),
})
)
Object.keys(env).forEach((key) => delete (env as any)[key])
})
it('should throw error when no API configuration provided', async () => {
const { env } = await import('@/lib/core/config/env')
Object.keys(env).forEach((key) => delete (env as any)[key])
await expect(generateEmbeddings(['test text'])).rejects.toThrow(
'OPENAI_API_KEY is not configured'
)
})
})
})