feat: AMD风格 capability 组合功能
- 新增 字段支持依赖声明(KvMetaValue、DeployParams)
- 新增 generateWorkerCodeWithDeps() 函数,支持依赖注入 codegen
- 实现递归依赖解析,自动 bundle 依赖代码到主 capability
- 循环依赖检测,防止无限递归
- execute 函数签名扩展为 (input, deps) => result
- 依赖包装为 async (params?) => result 函数
- 支持 schema 参数解析 for 依赖
- 向后兼容:无依赖 capability 不受影响
- 12个测试用例覆盖:基本依赖、多依赖、链式依赖、循环检测、兼容性、API集成
小橘 🍊 (NEKO Team)
This commit is contained in:
@@ -15,6 +15,7 @@ export interface DeployParams {
|
||||
description?: string // 一句话描述
|
||||
tags?: string[] // 标签
|
||||
examples?: string[] // 用法示例
|
||||
requires?: string[] // AMD 风格依赖
|
||||
}
|
||||
|
||||
export interface DeployResult {
|
||||
|
||||
+101
-7
@@ -6,6 +6,7 @@ import { KvStore } from '../kv.js'
|
||||
import { LruScheduler } from '../lru.js'
|
||||
import { CONFIG } from '../config.js'
|
||||
import { EmbeddingService, cosineSimilarity, mmrSelect } from '../embedding.js'
|
||||
import { generateWorkerCode, generateWorkerCodeWithDeps, type InputSchema, type DependencyInfo } from '../codegen.js'
|
||||
|
||||
export interface WorkerLoader {
|
||||
get(id: string, loader: () => any): { getEntrypoint(name?: string): { fetch(request: Request): Promise<Response> } }
|
||||
@@ -35,16 +36,109 @@ export class WorkerPool implements SigilBackend {
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').slice(0, this.config.HASH_LENGTH)
|
||||
}
|
||||
|
||||
async deploy(params: DeployParams): Promise<DeployResult> {
|
||||
const { name, code, schema, type, ttl, bindings, description, tags, examples } = params
|
||||
/**
|
||||
* 递归解析依赖,检测循环依赖
|
||||
*/
|
||||
private async resolveDependencies(
|
||||
requires: string[],
|
||||
visited = new Set<string>(),
|
||||
path: string[] = []
|
||||
): Promise<Record<string, DependencyInfo>> {
|
||||
const deps: Record<string, DependencyInfo> = {}
|
||||
|
||||
if (!code) {
|
||||
throw new Error('deploy: code is required (should be pre-generated by router)')
|
||||
for (const depName of requires) {
|
||||
// 检测循环依赖 - 在访问KV之前先检查
|
||||
if (visited.has(depName)) {
|
||||
const cycle = [...path, depName].join(' -> ')
|
||||
throw new Error(`Circular dependency detected: ${cycle}`)
|
||||
}
|
||||
|
||||
// 标记为已访问
|
||||
const newVisited = new Set(visited)
|
||||
newVisited.add(depName)
|
||||
const newPath = [...path, depName]
|
||||
|
||||
const depCode = await this.kv.getCode(depName)
|
||||
const depMeta = await this.kv.getMeta(depName)
|
||||
|
||||
if (!depCode || !depMeta) {
|
||||
throw new Error(`Dependency not found: ${depName}`)
|
||||
}
|
||||
|
||||
// 如果该依赖还有自己的依赖,递归解析
|
||||
if (depMeta.requires && depMeta.requires.length > 0) {
|
||||
const nestedDeps = await this.resolveDependencies(depMeta.requires, newVisited, newPath)
|
||||
Object.assign(deps, nestedDeps)
|
||||
}
|
||||
|
||||
// 对于 schema+execute 模式的依赖,从 code 中提取 execute body
|
||||
if (depMeta.schema) {
|
||||
// 从生成的 worker code 中提取用户的 execute body
|
||||
// 这个比较 hacky,实际应该存储原始的 execute body
|
||||
// 但为了兼容现有代码,我们从生成的代码中反向提取
|
||||
deps[depName] = {
|
||||
code: this.extractExecuteBodyFromWorkerCode(depCode),
|
||||
schema: depMeta.schema
|
||||
}
|
||||
} else {
|
||||
// 完整代码模式,直接使用
|
||||
deps[depName] = { code: depCode }
|
||||
}
|
||||
}
|
||||
|
||||
return deps
|
||||
}
|
||||
|
||||
/**
|
||||
* 从生成的 worker code 中提取用户的 execute body
|
||||
* 这是一个简化的实现,实际项目中应该分别存储原始 execute body
|
||||
*/
|
||||
private extractExecuteBodyFromWorkerCode(workerCode: string): string {
|
||||
// 查找 "const __result = await (async (input) => {" 后的内容
|
||||
const match = workerCode.match(/const __result = await \(async \(input\) => \{\s*([\s\S]*?)\s*\}\)\(input\);/)
|
||||
if (match && match[1]) {
|
||||
return match[1].trim()
|
||||
}
|
||||
// 如果无法提取,返回空函数
|
||||
return 'return null;'
|
||||
}
|
||||
|
||||
async deploy(params: DeployParams): Promise<DeployResult> {
|
||||
const { name, code, schema, execute, type, ttl, bindings, description, tags, examples, requires } = params
|
||||
|
||||
let finalCode: string
|
||||
|
||||
if (code) {
|
||||
// 模式 A:完整 Worker 代码
|
||||
finalCode = code
|
||||
} else {
|
||||
// 模式 B:schema + execute
|
||||
if (!execute) {
|
||||
throw new Error('deploy: execute is required when using schema mode')
|
||||
}
|
||||
|
||||
const finalSchema = schema || { type: 'object', properties: {} }
|
||||
|
||||
if (requires && requires.length > 0) {
|
||||
// 有依赖,解析并生成带依赖的代码
|
||||
try {
|
||||
// 先检查自引用循环
|
||||
const capabilityName = name || 'temp-name-for-cycle-check'
|
||||
const initialVisited = new Set([capabilityName])
|
||||
const deps = await this.resolveDependencies(requires, initialVisited, [capabilityName])
|
||||
finalCode = generateWorkerCodeWithDeps(finalSchema, execute, deps)
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to resolve dependencies: ${error.message}`)
|
||||
}
|
||||
} else {
|
||||
// 无依赖,使用原有逻辑
|
||||
finalCode = generateWorkerCode(finalSchema, execute)
|
||||
}
|
||||
}
|
||||
|
||||
let capability: string
|
||||
if (name === null) {
|
||||
const hash = await this.generateHash(code + Date.now())
|
||||
const hash = await this.generateHash(finalCode + Date.now())
|
||||
capability = `t-${hash}`
|
||||
} else {
|
||||
capability = name
|
||||
@@ -72,9 +166,9 @@ export class WorkerPool implements SigilBackend {
|
||||
const evictedCapability = evictedCapabilities[0]
|
||||
|
||||
// Write KV entries - code loaded dynamically at invoke time via LOADER
|
||||
await this.kv.setCode(capability, code)
|
||||
await this.kv.setCode(capability, finalCode)
|
||||
await this.kv.setMeta(capability, {
|
||||
type, ttl, created_at: now, bindings, description, tags, examples, schema,
|
||||
type, ttl, created_at: now, bindings, description, tags, examples, schema, requires,
|
||||
})
|
||||
await this.kv.setLru(capability, { last_access: now, access_count: 0, deployed: true })
|
||||
await this.kv.setRoute(capability, { worker_name: capability, subdomain: '' })
|
||||
|
||||
+116
@@ -10,6 +10,11 @@ export interface InputSchema {
|
||||
required?: string[]
|
||||
}
|
||||
|
||||
export interface DependencyInfo {
|
||||
code: string
|
||||
schema?: InputSchema
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 schema + execute body 生成完整 Worker 代码
|
||||
*/
|
||||
@@ -77,3 +82,114 @@ ${requiredChecks}
|
||||
}
|
||||
};`
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 schema + execute body + 依赖生成完整 Worker 代码(AMD 风格)
|
||||
*/
|
||||
export function generateWorkerCodeWithDeps(
|
||||
schema: InputSchema,
|
||||
executeBody: string,
|
||||
deps: Record<string, DependencyInfo>
|
||||
): string {
|
||||
const required = schema.required || []
|
||||
|
||||
// 生成参数解析 + 类型转换
|
||||
const parseLines: string[] = []
|
||||
for (const [name, prop] of Object.entries(schema.properties || {})) {
|
||||
if (prop.type === 'number') {
|
||||
parseLines.push(` if (raw.${name} !== undefined) input.${name} = Number(raw.${name});`)
|
||||
} else if (prop.type === 'boolean') {
|
||||
parseLines.push(` if (raw.${name} !== undefined) input.${name} = raw.${name} === 'true' || raw.${name} === true;`)
|
||||
} else {
|
||||
parseLines.push(` if (raw.${name} !== undefined) input.${name} = raw.${name};`)
|
||||
}
|
||||
// 默认值
|
||||
if (prop.default !== undefined) {
|
||||
parseLines.push(` if (input.${name} === undefined) input.${name} = ${JSON.stringify(prop.default)};`)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成 required 校验
|
||||
const requiredChecks = required.map(name =>
|
||||
` if (input.${name} === undefined) return new Response(JSON.stringify({error: "Missing required parameter: ${name}"}), {status: 400, headers: {"Content-Type": "application/json"}});`
|
||||
).join('\n')
|
||||
|
||||
// 生成依赖函数
|
||||
const depsCode = Object.entries(deps).map(([depName, depInfo]) => {
|
||||
const depSchema = depInfo.schema
|
||||
if (!depSchema) {
|
||||
// 无 schema,直接执行
|
||||
return ` '${depName}': async (params = {}) => {
|
||||
const input = params;
|
||||
${depInfo.code}
|
||||
}`
|
||||
}
|
||||
|
||||
// 有 schema,需要参数解析
|
||||
const depParseLines: string[] = []
|
||||
for (const [name, prop] of Object.entries(depSchema.properties || {})) {
|
||||
if (prop.type === 'number') {
|
||||
depParseLines.push(` if (params.${name} !== undefined) input.${name} = Number(params.${name});`)
|
||||
} else if (prop.type === 'boolean') {
|
||||
depParseLines.push(` if (params.${name} !== undefined) input.${name} = params.${name} === 'true' || params.${name} === true;`)
|
||||
} else {
|
||||
depParseLines.push(` if (params.${name} !== undefined) input.${name} = params.${name};`)
|
||||
}
|
||||
// 默认值
|
||||
if (prop.default !== undefined) {
|
||||
depParseLines.push(` if (input.${name} === undefined) input.${name} = ${JSON.stringify(prop.default)};`)
|
||||
}
|
||||
}
|
||||
|
||||
return ` '${depName}': async (params = {}) => {
|
||||
const input = {};
|
||||
${depParseLines.join('\n')}
|
||||
${depInfo.code}
|
||||
}`
|
||||
}).join(',\n')
|
||||
|
||||
return `export default {
|
||||
async fetch(request) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
let raw = {};
|
||||
|
||||
// Parse input from query params or JSON body
|
||||
if (request.method === 'POST' || request.method === 'PUT') {
|
||||
try { raw = await request.json(); } catch(e) { raw = {}; }
|
||||
}
|
||||
// Query params override/merge
|
||||
for (const [k, v] of url.searchParams.entries()) {
|
||||
raw[k] = v;
|
||||
}
|
||||
|
||||
const input = {};
|
||||
${parseLines.join('\n')}
|
||||
|
||||
// Required field validation
|
||||
${requiredChecks}
|
||||
|
||||
// AMD deps - 每个依赖内联为函数
|
||||
const deps = {
|
||||
${depsCode}
|
||||
};
|
||||
|
||||
// Execute user function (with deps)
|
||||
const __result = await (async (input, deps) => {
|
||||
${executeBody}
|
||||
})(input, deps);
|
||||
|
||||
// Ensure string output
|
||||
const output = typeof __result === 'string' ? __result : JSON.stringify(__result);
|
||||
return new Response(output, {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
} catch (e) {
|
||||
return new Response(JSON.stringify({ error: e.message || "Internal error" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
}
|
||||
};`
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface KvMetaValue {
|
||||
tags?: string[]
|
||||
examples?: string[]
|
||||
schema?: InputSchema
|
||||
requires?: string[]
|
||||
}
|
||||
|
||||
export interface KvLruValue {
|
||||
|
||||
@@ -74,6 +74,7 @@ async function handleDeploy(request: Request, env: RouterEnv): Promise<Response>
|
||||
description?: string
|
||||
tags?: string[]
|
||||
examples?: string[]
|
||||
requires?: string[]
|
||||
}
|
||||
|
||||
// Route validation
|
||||
@@ -110,6 +111,7 @@ async function handleDeploy(request: Request, env: RouterEnv): Promise<Response>
|
||||
description: body.description,
|
||||
tags: body.tags,
|
||||
examples: body.examples,
|
||||
requires: body.requires,
|
||||
})
|
||||
|
||||
// Set cooldown after successful deploy
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
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'])
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user