69507fa766
- 新增 字段支持依赖声明(KvMetaValue、DeployParams)
- 新增 generateWorkerCodeWithDeps() 函数,支持依赖注入 codegen
- 实现递归依赖解析,自动 bundle 依赖代码到主 capability
- 循环依赖检测,防止无限递归
- execute 函数签名扩展为 (input, deps) => result
- 依赖包装为 async (params?) => result 函数
- 支持 schema 参数解析 for 依赖
- 向后兼容:无依赖 capability 不受影响
- 12个测试用例覆盖:基本依赖、多依赖、链式依赖、循环检测、兼容性、API集成
小橘 🍊 (NEKO Team)
358 lines
11 KiB
TypeScript
358 lines
11 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { WorkerPool } from '../src/backend/worker-pool.js'
|
|
import { AuthModule } from '../src/auth.js'
|
|
import { createMockKv, createMockLoader, MockEmbeddingService } from './setup.js'
|
|
import { handleRequest } from '../src/router.js'
|
|
import { KvStore } from '../src/kv.js'
|
|
|
|
describe('AMD Capabilities', () => {
|
|
let kv: KVNamespace
|
|
let kvStore: KvStore
|
|
let loader: ReturnType<typeof createMockLoader>
|
|
let embeddingService: MockEmbeddingService
|
|
let backend: WorkerPool
|
|
let auth: AuthModule
|
|
let env: any
|
|
|
|
beforeEach(() => {
|
|
kv = createMockKv()
|
|
kvStore = new KvStore(kv)
|
|
loader = createMockLoader()
|
|
embeddingService = new MockEmbeddingService()
|
|
backend = new WorkerPool(kv, loader.loader, embeddingService)
|
|
auth = new AuthModule(kvStore)
|
|
|
|
env = {
|
|
SIGIL_KV: kv,
|
|
backend,
|
|
auth,
|
|
kv: kvStore,
|
|
}
|
|
|
|
// Set deploy token
|
|
auth.setToken('test-token')
|
|
})
|
|
|
|
describe('Basic dependency injection', () => {
|
|
it('should deploy capability with single dependency', async () => {
|
|
// Deploy dependency first
|
|
await backend.deploy({
|
|
name: 'dep-a',
|
|
execute: 'return "value-a";',
|
|
type: 'normal',
|
|
description: 'Dependency A',
|
|
})
|
|
|
|
// Deploy capability that requires dep-a
|
|
const result = await backend.deploy({
|
|
name: 'main-cap',
|
|
execute: 'const result = await deps["dep-a"](); return `Main: ${result}`;',
|
|
type: 'normal',
|
|
description: 'Main capability',
|
|
requires: ['dep-a'],
|
|
})
|
|
|
|
expect(result.capability).toBe('main-cap')
|
|
expect(result.url).toMatch(/\/run\/main-cap$/)
|
|
})
|
|
|
|
it('should generate correct code structure with dependencies', async () => {
|
|
// Deploy dependency
|
|
await backend.deploy({
|
|
name: 'token-provider',
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
service: { type: 'string', default: 'github' }
|
|
},
|
|
},
|
|
execute: 'return `token-${input.service}`;',
|
|
type: 'normal',
|
|
})
|
|
|
|
await backend.deploy({
|
|
name: 'api-client',
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
endpoint: { type: 'string' }
|
|
},
|
|
required: ['endpoint'],
|
|
},
|
|
execute: `
|
|
const token = await deps["token-provider"]({ service: "github" });
|
|
return { endpoint: input.endpoint, token };
|
|
`,
|
|
type: 'normal',
|
|
requires: ['token-provider'],
|
|
})
|
|
|
|
const code = await kvStore.getCode('api-client')
|
|
expect(code).toContain('const deps = {')
|
|
expect(code).toContain("'token-provider':")
|
|
expect(code).toContain('async (params = {}) =>')
|
|
expect(code).toContain('(input, deps) =>')
|
|
})
|
|
})
|
|
|
|
describe('Multiple dependencies', () => {
|
|
it('should handle multiple dependencies', async () => {
|
|
// Deploy dependencies
|
|
await backend.deploy({
|
|
name: 'dep-b',
|
|
execute: 'return "value-b";',
|
|
type: 'normal',
|
|
})
|
|
|
|
await backend.deploy({
|
|
name: 'dep-c',
|
|
execute: 'return "value-c";',
|
|
type: 'normal',
|
|
})
|
|
|
|
// Deploy main capability
|
|
const result = await backend.deploy({
|
|
name: 'multi-dep',
|
|
execute: `
|
|
const b = await deps["dep-b"]();
|
|
const c = await deps["dep-c"]();
|
|
return \`\${b} and \${c}\`;
|
|
`,
|
|
type: 'normal',
|
|
requires: ['dep-b', 'dep-c'],
|
|
})
|
|
|
|
expect(result.capability).toBe('multi-dep')
|
|
|
|
const code = await kvStore.getCode('multi-dep')
|
|
expect(code).toContain("'dep-b':")
|
|
expect(code).toContain("'dep-c':")
|
|
})
|
|
})
|
|
|
|
describe('Chain dependencies', () => {
|
|
it('should handle chained dependencies (A requires B, B requires C)', async () => {
|
|
// Deploy C (no deps)
|
|
await backend.deploy({
|
|
name: 'base-service',
|
|
execute: 'return "base-value";',
|
|
type: 'normal',
|
|
})
|
|
|
|
// Deploy B (requires C)
|
|
await backend.deploy({
|
|
name: 'middleware',
|
|
execute: `
|
|
const base = await deps["base-service"]();
|
|
return \`middleware(\${base})\`;
|
|
`,
|
|
type: 'normal',
|
|
requires: ['base-service'],
|
|
})
|
|
|
|
// Deploy A (requires B)
|
|
const result = await backend.deploy({
|
|
name: 'top-level',
|
|
execute: `
|
|
const mid = await deps["middleware"]();
|
|
return \`top(\${mid})\`;
|
|
`,
|
|
type: 'normal',
|
|
requires: ['middleware'],
|
|
})
|
|
|
|
expect(result.capability).toBe('top-level')
|
|
|
|
// Should contain both direct and transitive dependencies
|
|
const code = await kvStore.getCode('top-level')
|
|
expect(code).toContain("'middleware':")
|
|
expect(code).toContain("'base-service':")
|
|
})
|
|
})
|
|
|
|
describe('Circular dependency detection', () => {
|
|
it('should detect simple circular dependency via self-reference', async () => {
|
|
// Try to create a capability that depends on itself
|
|
await expect(backend.deploy({
|
|
name: 'self-ref',
|
|
execute: 'const self = await deps["self-ref"](); return `self-${self}`;',
|
|
type: 'normal',
|
|
requires: ['self-ref'],
|
|
})).rejects.toThrow('Circular dependency detected: self-ref -> self-ref')
|
|
})
|
|
|
|
it('should work with real dependencies but detect actual cycle', async () => {
|
|
// Create a proper test environment for cycle detection
|
|
// Step 1: Create base capabilities
|
|
await backend.deploy({ name: 'base-1', execute: 'return "base1";', type: 'normal' })
|
|
await backend.deploy({ name: 'base-2', execute: 'return "base2";', type: 'normal' })
|
|
|
|
// Step 2: Create mid-level that depends on base
|
|
await backend.deploy({
|
|
name: 'mid-1',
|
|
execute: 'const b1 = await deps["base-1"](); return `mid1-${b1}`;',
|
|
type: 'normal',
|
|
requires: ['base-1']
|
|
})
|
|
|
|
// Step 3: Now let's simulate creating a cycle by having a test resolver
|
|
// that tracks dependencies in a simple way
|
|
const visited = new Set<string>(['base-1'])
|
|
const path = ['base-1']
|
|
|
|
// Simulate what would happen if base-1 tried to depend on mid-1
|
|
if (visited.has('mid-1')) {
|
|
const cycle = [...path, 'mid-1'].join(' -> ')
|
|
expect(cycle).toContain('base-1 -> mid-1')
|
|
}
|
|
|
|
// The actual way to test would be to create this scenario:
|
|
// Let's remove base-1 and try to make it depend on mid-1
|
|
await backend.remove('base-1')
|
|
|
|
// This should now detect circular dependency: base-1 -> mid-1 -> base-1
|
|
await expect(backend.deploy({
|
|
name: 'base-1',
|
|
execute: 'const mid = await deps["mid-1"](); return `new-base1-${mid}`;',
|
|
type: 'normal',
|
|
requires: ['mid-1']
|
|
})).rejects.toThrow('Circular dependency detected')
|
|
})
|
|
})
|
|
|
|
describe('Backward compatibility', () => {
|
|
it('should not affect capabilities without dependencies', async () => {
|
|
const result = await backend.deploy({
|
|
name: 'no-deps',
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
message: { type: 'string', default: 'hello' }
|
|
},
|
|
},
|
|
execute: 'return `No deps: ${input.message}`;',
|
|
type: 'normal',
|
|
})
|
|
|
|
expect(result.capability).toBe('no-deps')
|
|
|
|
const code = await kvStore.getCode('no-deps')
|
|
expect(code).not.toContain('const deps = {')
|
|
expect(code).toContain('(input) => {')
|
|
})
|
|
|
|
it('should handle full code deployment mode', async () => {
|
|
const workerCode = `
|
|
export default {
|
|
async fetch(request) {
|
|
return new Response('custom worker code');
|
|
}
|
|
};
|
|
`
|
|
|
|
const result = await backend.deploy({
|
|
name: 'custom-worker',
|
|
code: workerCode,
|
|
type: 'normal',
|
|
})
|
|
|
|
expect(result.capability).toBe('custom-worker')
|
|
|
|
const code = await kvStore.getCode('custom-worker')
|
|
expect(code).toBe(workerCode)
|
|
})
|
|
})
|
|
|
|
describe('Error handling', () => {
|
|
it('should fail when dependency not found', async () => {
|
|
await expect(backend.deploy({
|
|
name: 'missing-dep',
|
|
execute: 'return await deps["nonexistent"]();',
|
|
type: 'normal',
|
|
requires: ['nonexistent'],
|
|
})).rejects.toThrow('Dependency not found: nonexistent')
|
|
})
|
|
|
|
it('should handle empty requires array', async () => {
|
|
const result = await backend.deploy({
|
|
name: 'empty-requires',
|
|
execute: 'return "no deps";',
|
|
type: 'normal',
|
|
requires: [],
|
|
})
|
|
|
|
expect(result.capability).toBe('empty-requires')
|
|
|
|
const code = await kvStore.getCode('empty-requires')
|
|
expect(code).not.toContain('const deps = {')
|
|
})
|
|
})
|
|
|
|
describe('API integration', () => {
|
|
it('should accept requires field via router', async () => {
|
|
// Reset cooldown before test by setting last deploy time to far past
|
|
await kvStore.setLastDeployTime(Date.now() - 60000) // 1 minute ago
|
|
|
|
// Deploy dependency via API
|
|
const depRequest = new Request('https://test.com/_api/deploy', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer test-token',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
name: 'api-dep',
|
|
execute: 'return "api-dependency";',
|
|
type: 'normal',
|
|
}),
|
|
})
|
|
|
|
const depResponse = await handleRequest(depRequest, env)
|
|
expect(depResponse.status).toBe(201)
|
|
|
|
// Reset cooldown again
|
|
await kvStore.setLastDeployTime(Date.now() - 60000) // 1 minute ago
|
|
|
|
// Deploy main capability with requires
|
|
const mainRequest = new Request('https://test.com/_api/deploy', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer test-token',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
name: 'api-main',
|
|
execute: 'const dep = await deps["api-dep"](); return `Main: ${dep}`;',
|
|
type: 'normal',
|
|
requires: ['api-dep'],
|
|
}),
|
|
})
|
|
|
|
const mainResponse = await handleRequest(mainRequest, env)
|
|
expect(mainResponse.status).toBe(201)
|
|
|
|
const result = await mainResponse.json()
|
|
expect(result.capability).toBe('api-main')
|
|
})
|
|
|
|
it('should store requires in metadata', async () => {
|
|
// Deploy dependency first
|
|
await backend.deploy({
|
|
name: 'some-dep',
|
|
execute: 'return "dependency";',
|
|
type: 'normal',
|
|
})
|
|
|
|
// Then deploy main capability
|
|
await backend.deploy({
|
|
name: 'meta-test',
|
|
execute: 'return await deps["some-dep"]();',
|
|
type: 'normal',
|
|
requires: ['some-dep'],
|
|
})
|
|
|
|
const meta = await kvStore.getMeta('meta-test')
|
|
expect(meta?.requires).toEqual(['some-dep'])
|
|
})
|
|
})
|
|
}) |