Files
sigil/test/s14-unified-invoke.test.ts
xiaomo 9787bb7f39 feat: unified invoke endpoint — sigil.* builtin capabilities (refs #1)
- Add POST /_api/invoke unified entry point for all capabilities
- Support builtin sigil.* capabilities: discover, deploy, remove, inspect, list, status
- External capabilities routed through backend.invoke() as before
- Comprehensive test coverage in s14-unified-invoke.test.ts
- All existing routes preserved for backward compatibility
- Error handling for invalid JSON and missing capability field
2026-04-18 07:18:15 +00:00

330 lines
10 KiB
TypeScript

import { describe, test, expect, beforeEach } from 'vitest'
import { createMockKv, createMockLoader, MockEmbeddingService } from './setup.js'
import { WorkerPool } from '../src/backend/worker-pool.js'
import { AuthModule } from '../src/auth.js'
import { KvStore } from '../src/kv.js'
import { handleRequest } from '../src/router.js'
describe('Unified Invoke API', () => {
let mockKv: KVNamespace
let mockLoader: ReturnType<typeof createMockLoader>
let mockEmbed: MockEmbeddingService
let pool: WorkerPool
let auth: AuthModule
let kv: KvStore
beforeEach(async () => {
mockKv = createMockKv()
mockLoader = createMockLoader({
invokeResponse: (workerId, request) => {
// Mock external capability responses
return new Response(JSON.stringify({ result: 'external success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
})
mockEmbed = new MockEmbeddingService()
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
kv = new KvStore(mockKv)
auth = new AuthModule(kv)
// Set unified deploy token
await auth.setToken('valid-token')
})
const makeRequest = (method: string, path: string, options?: {
body?: unknown
token?: string
headers?: Record<string, string>
}): Request => {
const url = `https://sigil.shazhou.workers.dev${path}`
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...options?.headers,
}
if (options?.token) {
headers['Authorization'] = `Bearer ${options.token}`
}
const init: RequestInit = {
method,
headers,
}
if (options?.body !== undefined) {
init.body = JSON.stringify(options.body)
}
return new Request(url, init)
}
// Test builtin capabilities
describe('Builtin Capabilities', () => {
test('sigil.discover - query capabilities', async () => {
// Deploy a test capability first
await pool.deploy({
name: 'hello',
code: "export default { fetch() { return new Response('hello') } }",
type: 'normal'
})
const request = makeRequest('POST', '/_api/invoke', {
token: 'valid-token',
body: {
capability: 'sigil.discover',
params: {
q: 'hello',
limit: 5
}
}
})
const response = await handleRequest(request, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(response.status).toBe(200)
const result = await response.json()
expect(result.items).toBeInstanceOf(Array)
expect(result.total).toBeGreaterThan(0)
})
test('sigil.deploy - deploy capability', async () => {
const request = makeRequest('POST', '/_api/invoke', {
token: 'valid-token',
body: {
capability: 'sigil.deploy',
params: {
name: 'test-cap',
execute: 'return "hello world"',
type: 'normal'
}
}
})
const response = await handleRequest(request, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(response.status).toBe(201)
const result = await response.json()
expect(result.capability).toBe('test-cap')
})
test('sigil.remove - remove capability', async () => {
// Deploy a capability first
await pool.deploy({
name: 'test-cap',
code: "export default { fetch() { return new Response('test') } }",
type: 'normal'
})
const request = makeRequest('POST', '/_api/invoke', {
token: 'valid-token',
body: {
capability: 'sigil.remove',
params: {
capability: 'test-cap'
}
}
})
const response = await handleRequest(request, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(response.status).toBe(200)
const result = await response.json()
expect(result.removed).toBe('test-cap')
})
test('sigil.inspect - inspect capability', async () => {
// Deploy a capability first
await pool.deploy({
name: 'hello',
code: "export default { fetch() { return new Response('hello') } }",
type: 'normal'
})
const request = makeRequest('POST', '/_api/invoke', {
token: 'valid-token',
body: {
capability: 'sigil.inspect',
params: {
capability: 'hello'
}
}
})
const response = await handleRequest(request, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(response.status).toBe(200)
const result = await response.json()
expect(result.capability).toBe('hello')
expect(result.type).toBe('normal')
})
test('sigil.list - list all capabilities', async () => {
// Deploy some test capabilities
await pool.deploy({
name: 'cap1',
code: "export default { fetch() { return new Response('cap1') } }",
type: 'normal'
})
await pool.deploy({
name: 'cap2',
code: "export default { fetch() { return new Response('cap2') } }",
type: 'persistent'
})
const request = makeRequest('POST', '/_api/invoke', {
token: 'valid-token',
body: {
capability: 'sigil.list',
params: {
limit: 10
}
}
})
const response = await handleRequest(request, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(response.status).toBe(200)
const result = await response.json()
expect(result.items).toBeInstanceOf(Array)
expect(result.items.length).toBeGreaterThanOrEqual(2)
})
test('sigil.status - get system status', async () => {
const request = makeRequest('POST', '/_api/invoke', {
token: 'valid-token',
body: {
capability: 'sigil.status',
params: {}
}
})
const response = await handleRequest(request, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(response.status).toBe(200)
const result = await response.json()
expect(result).toHaveProperty('backend')
expect(result).toHaveProperty('total_slots')
})
test('unknown builtin capability should return 404', async () => {
const request = makeRequest('POST', '/_api/invoke', {
token: 'valid-token',
body: {
capability: 'sigil.unknown',
params: {}
}
})
const response = await handleRequest(request, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(response.status).toBe(404)
const result = await response.json()
expect(result.error).toContain('Builtin capability not found')
})
})
// Test external capabilities
describe('External Capabilities', () => {
test('should invoke external capability', async () => {
// Deploy a capability first
await pool.deploy({
name: 'external-cap',
code: "export default { fetch() { return new Response(JSON.stringify({result: 'external success'}), {headers: {'Content-Type': 'application/json'}}) } }",
type: 'normal'
})
const request = makeRequest('POST', '/_api/invoke', {
token: 'valid-token',
body: {
capability: 'external-cap',
params: {
input: 'test data'
}
}
})
const response = await handleRequest(request, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(response.status).toBe(200)
const result = await response.json()
expect(result.result).toBe('external success')
})
test('external capability not found should return error', async () => {
const request = makeRequest('POST', '/_api/invoke', {
token: 'valid-token',
body: {
capability: 'missing-cap',
params: {}
}
})
const response = await handleRequest(request, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(response.status).toBeGreaterThanOrEqual(400)
})
})
// Test error cases
describe('Error Cases', () => {
test('missing capability field should return 400', async () => {
const request = makeRequest('POST', '/_api/invoke', {
token: 'valid-token',
body: {
params: { test: 'data' }
}
})
const response = await handleRequest(request, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(response.status).toBe(400)
const result = await response.json()
expect(result.error).toBe('capability field is required')
})
test('missing auth should return 401', async () => {
const request = makeRequest('POST', '/_api/invoke', {
body: {
capability: 'sigil.status',
params: {}
}
})
const response = await handleRequest(request, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(response.status).toBe(401)
})
test('invalid JSON should return 400', async () => {
const request = new Request('https://sigil.shazhou.workers.dev/_api/invoke', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer valid-token'
},
body: 'invalid json'
})
const response = await handleRequest(request, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(response.status).toBe(400)
const result = await response.json()
expect(result.error).toBe('Invalid JSON in request body')
})
})
// Test compatibility with existing routes
describe('Backward Compatibility', () => {
test('existing routes should still work', async () => {
// Test old health route
const healthRequest = makeRequest('GET', '/_health')
const healthResponse = await handleRequest(healthRequest, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(healthResponse.status).toBe(200)
// Test old query route
const queryRequest = makeRequest('GET', '/_api/query?q=test', { token: 'valid-token' })
const queryResponse = await handleRequest(queryRequest, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(queryResponse.status).toBe(200)
})
})
})