9787bb7f39
- 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
330 lines
10 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
}) |