feat: initial release v0.3.0
This commit is contained in:
15
tests/unit/assistant-platform/registry.test.ts
Normal file
15
tests/unit/assistant-platform/registry.test.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getAssistantSkill, isAssistantId } from '@/lib/assistant-platform'
|
||||
|
||||
describe('assistant-platform registry', () => {
|
||||
it('recognizes supported assistant ids', () => {
|
||||
expect(isAssistantId('api-config-template')).toBe(true)
|
||||
expect(isAssistantId('tutorial')).toBe(true)
|
||||
expect(isAssistantId('unknown')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns registered skills', () => {
|
||||
expect(getAssistantSkill('api-config-template').id).toBe('api-config-template')
|
||||
expect(getAssistantSkill('tutorial').id).toBe('tutorial')
|
||||
})
|
||||
})
|
||||
46
tests/unit/assistant-platform/runtime.test.ts
Normal file
46
tests/unit/assistant-platform/runtime.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const getUserModelConfigMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({ analysisModel: null })),
|
||||
)
|
||||
|
||||
vi.mock('@/lib/config-service', () => ({
|
||||
getUserModelConfig: getUserModelConfigMock,
|
||||
}))
|
||||
|
||||
import { AssistantPlatformError } from '@/lib/assistant-platform'
|
||||
import { createAssistantChatResponse } from '@/lib/assistant-platform/runtime'
|
||||
|
||||
describe('assistant-platform runtime', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('throws invalid request when messages payload is malformed', async () => {
|
||||
await expect(createAssistantChatResponse({
|
||||
userId: 'user-1',
|
||||
assistantId: 'api-config-template',
|
||||
context: {},
|
||||
messages: { invalid: true },
|
||||
})).rejects.toMatchObject({
|
||||
code: 'ASSISTANT_INVALID_REQUEST',
|
||||
} as Partial<AssistantPlatformError>)
|
||||
})
|
||||
|
||||
it('throws missing model when analysisModel is not configured', async () => {
|
||||
await expect(createAssistantChatResponse({
|
||||
userId: 'user-1',
|
||||
assistantId: 'api-config-template',
|
||||
context: {
|
||||
providerId: 'openai-compatible:oa-1',
|
||||
},
|
||||
messages: [{
|
||||
id: 'u1',
|
||||
role: 'user',
|
||||
parts: [{ type: 'text', text: 'hello' }],
|
||||
}],
|
||||
})).rejects.toMatchObject({
|
||||
code: 'ASSISTANT_MODEL_NOT_CONFIGURED',
|
||||
} as Partial<AssistantPlatformError>)
|
||||
})
|
||||
})
|
||||
230
tests/unit/assistant-platform/skills-api-config-template.test.ts
Normal file
230
tests/unit/assistant-platform/skills-api-config-template.test.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AssistantRuntimeContext } from '@/lib/assistant-platform'
|
||||
|
||||
const saveModelTemplateConfigurationMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({ modelKey: 'openai-compatible:oa-1::veo3.1' })),
|
||||
)
|
||||
|
||||
vi.mock('@/lib/user-api/model-template/save', () => ({
|
||||
saveModelTemplateConfiguration: saveModelTemplateConfigurationMock,
|
||||
}))
|
||||
|
||||
import { apiConfigTemplateSkill } from '@/lib/assistant-platform/skills/api-config-template'
|
||||
|
||||
function buildRuntimeContext(): AssistantRuntimeContext {
|
||||
return {
|
||||
userId: 'user-1',
|
||||
assistantId: 'api-config-template',
|
||||
context: {
|
||||
providerId: 'openai-compatible:oa-1',
|
||||
},
|
||||
analysisModelKey: 'openrouter::gpt-5-mini',
|
||||
resolvedModel: {
|
||||
providerId: 'openrouter',
|
||||
providerKey: 'openrouter',
|
||||
modelId: 'gpt-5-mini',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('assistant-platform api-config-template skill', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('returns invalid when template fails schema validation', async () => {
|
||||
const tools = apiConfigTemplateSkill.tools?.(buildRuntimeContext())
|
||||
expect(tools).toBeTruthy()
|
||||
const saveTool = tools?.saveModelTemplate
|
||||
expect(saveTool).toBeTruthy()
|
||||
if (!saveTool?.execute) {
|
||||
throw new Error('saveModelTemplate.execute is required for test')
|
||||
}
|
||||
|
||||
const result = await saveTool.execute({
|
||||
modelId: 'veo3.1',
|
||||
name: 'Veo 3.1',
|
||||
type: 'video',
|
||||
compatMediaTemplate: {
|
||||
version: 1,
|
||||
mediaType: 'video',
|
||||
mode: 'async',
|
||||
create: {
|
||||
method: 'POST',
|
||||
path: '/v2/videos/generations',
|
||||
},
|
||||
response: {
|
||||
taskIdPath: '$.task_id',
|
||||
},
|
||||
},
|
||||
}, {} as never)
|
||||
|
||||
expect(result.status).toBe('invalid')
|
||||
expect(result.code).toBe('MODEL_TEMPLATE_INVALID')
|
||||
expect(saveModelTemplateConfigurationMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('saves template when payload is valid', async () => {
|
||||
const tools = apiConfigTemplateSkill.tools?.(buildRuntimeContext())
|
||||
expect(tools).toBeTruthy()
|
||||
const saveTool = tools?.saveModelTemplate
|
||||
expect(saveTool).toBeTruthy()
|
||||
if (!saveTool?.execute) {
|
||||
throw new Error('saveModelTemplate.execute is required for test')
|
||||
}
|
||||
|
||||
const result = await saveTool.execute({
|
||||
modelId: 'veo3.1',
|
||||
name: 'Veo 3.1',
|
||||
type: 'video',
|
||||
compatMediaTemplate: {
|
||||
version: 1,
|
||||
mediaType: 'video',
|
||||
mode: 'async',
|
||||
create: {
|
||||
method: 'POST',
|
||||
path: '/v2/videos/generations',
|
||||
contentType: 'application/json',
|
||||
bodyTemplate: {
|
||||
model: '{{model}}',
|
||||
prompt: '{{prompt}}',
|
||||
},
|
||||
},
|
||||
status: {
|
||||
method: 'GET',
|
||||
path: '/v2/videos/generations/{{task_id}}',
|
||||
},
|
||||
response: {
|
||||
taskIdPath: '$.task_id',
|
||||
statusPath: '$.status',
|
||||
outputUrlPath: '$.video_url',
|
||||
},
|
||||
polling: {
|
||||
intervalMs: 3000,
|
||||
timeoutMs: 180000,
|
||||
doneStates: ['done'],
|
||||
failStates: ['failed'],
|
||||
},
|
||||
},
|
||||
}, {} as never)
|
||||
|
||||
expect(result.status).toBe('saved')
|
||||
expect(result.savedModelKey).toBe('openai-compatible:oa-1::veo3.1')
|
||||
expect(saveModelTemplateConfigurationMock).toHaveBeenCalledWith({
|
||||
userId: 'user-1',
|
||||
providerId: 'openai-compatible:oa-1',
|
||||
modelId: 'veo3.1',
|
||||
name: 'Veo 3.1',
|
||||
type: 'video',
|
||||
template: expect.objectContaining({
|
||||
mediaType: 'video',
|
||||
}),
|
||||
source: 'ai',
|
||||
})
|
||||
})
|
||||
|
||||
it('saves multiple templates when batch payload is valid', async () => {
|
||||
const tools = apiConfigTemplateSkill.tools?.(buildRuntimeContext())
|
||||
expect(tools).toBeTruthy()
|
||||
const batchTool = tools?.saveModelTemplates
|
||||
expect(batchTool).toBeTruthy()
|
||||
if (!batchTool?.execute) {
|
||||
throw new Error('saveModelTemplates.execute is required for test')
|
||||
}
|
||||
|
||||
const result = await batchTool.execute({
|
||||
models: [
|
||||
{
|
||||
modelId: 'veo3-fast',
|
||||
name: 'Veo 3 Fast',
|
||||
type: 'video',
|
||||
compatMediaTemplate: {
|
||||
version: 1,
|
||||
mediaType: 'video',
|
||||
mode: 'async',
|
||||
create: {
|
||||
method: 'POST',
|
||||
path: '/video/create',
|
||||
contentType: 'application/json',
|
||||
bodyTemplate: {
|
||||
model: '{{model}}',
|
||||
prompt: '{{prompt}}',
|
||||
images: ['{{image}}'],
|
||||
},
|
||||
},
|
||||
status: {
|
||||
method: 'GET',
|
||||
path: '/video/query?id={{task_id}}',
|
||||
},
|
||||
response: {
|
||||
taskIdPath: '$.id',
|
||||
statusPath: '$.status',
|
||||
outputUrlPath: '$.video_url',
|
||||
},
|
||||
polling: {
|
||||
intervalMs: 5000,
|
||||
timeoutMs: 600000,
|
||||
doneStates: ['completed'],
|
||||
failStates: ['failed'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
modelId: 'veo3.1-fast',
|
||||
name: 'Veo 3.1 Fast',
|
||||
type: 'video',
|
||||
compatMediaTemplate: {
|
||||
version: 1,
|
||||
mediaType: 'video',
|
||||
mode: 'async',
|
||||
create: {
|
||||
method: 'POST',
|
||||
path: '/video/create',
|
||||
contentType: 'application/json',
|
||||
bodyTemplate: {
|
||||
model: '{{model}}',
|
||||
prompt: '{{prompt}}',
|
||||
images: ['{{image}}'],
|
||||
},
|
||||
},
|
||||
status: {
|
||||
method: 'GET',
|
||||
path: '/video/query?id={{task_id}}',
|
||||
},
|
||||
response: {
|
||||
taskIdPath: '$.id',
|
||||
statusPath: '$.status',
|
||||
outputUrlPath: '$.video_url',
|
||||
},
|
||||
polling: {
|
||||
intervalMs: 5000,
|
||||
timeoutMs: 600000,
|
||||
doneStates: ['completed'],
|
||||
failStates: ['failed'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}, {} as never)
|
||||
|
||||
expect(result.status).toBe('saved')
|
||||
expect(result.savedModelKeys).toHaveLength(2)
|
||||
expect(saveModelTemplateConfigurationMock).toHaveBeenCalledTimes(2)
|
||||
expect(saveModelTemplateConfigurationMock).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
||||
modelId: 'veo3-fast',
|
||||
name: 'Veo 3 Fast',
|
||||
providerId: 'openai-compatible:oa-1',
|
||||
userId: 'user-1',
|
||||
type: 'video',
|
||||
source: 'ai',
|
||||
}))
|
||||
expect(saveModelTemplateConfigurationMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
||||
modelId: 'veo3.1-fast',
|
||||
name: 'Veo 3.1 Fast',
|
||||
providerId: 'openai-compatible:oa-1',
|
||||
userId: 'user-1',
|
||||
type: 'video',
|
||||
source: 'ai',
|
||||
}))
|
||||
})
|
||||
})
|
||||
21
tests/unit/assistant-platform/system-prompts.test.ts
Normal file
21
tests/unit/assistant-platform/system-prompts.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { renderAssistantSystemPrompt } from '@/lib/assistant-platform/system-prompts'
|
||||
|
||||
describe('assistant-platform system prompts', () => {
|
||||
it('loads api-config-template prompt from lib/prompts/skills and injects providerId', () => {
|
||||
const prompt = renderAssistantSystemPrompt('api-config-template', {
|
||||
providerId: 'openai-compatible:oa-1',
|
||||
})
|
||||
|
||||
expect(prompt).toContain('你是 API 配置助手')
|
||||
expect(prompt).toContain('当前 providerId=openai-compatible:oa-1')
|
||||
expect(prompt).not.toContain('{{providerId}}')
|
||||
})
|
||||
|
||||
it('loads tutorial prompt from lib/prompts/skills', () => {
|
||||
const prompt = renderAssistantSystemPrompt('tutorial')
|
||||
|
||||
expect(prompt).toContain('你是产品教程助手')
|
||||
expect(prompt).toContain('禁止编造不存在的页面')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user