Files
sigil/test/codegen.test.ts
T
xiaoju 3709fae5e1 fix: /run/{name} → 302 redirect, bypass CF same-zone fetch limitation
CF Workers cannot fetch() other workers on the same .workers.dev zone.
This caused all /run/{name} routes to return Cloudflare's HTML 404
instead of proxying to the sub-worker.

Fix: replace inline fetch() proxy with a redirect-based approach:
- Default (browser/curl): 302 redirect to sub-worker URL
- Accept: application/json: return JSON with {url, capability, cold_start}

LRU bookkeeping (page-in, access count) still happens in Sigil before
the redirect, so cold capabilities are warmed up transparently.

New backend method: resolveInvoke() — same LRU/page-in logic as invoke()
but returns route info instead of executing the subrequest.

Fixes: https://sigil.shazhou.workers.dev/run/* returning CF 404
Reported-by: 小墨 🖊️
2026-04-03 09:15:58 +00:00

224 lines
7.6 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { generateWorkerCode } from '../src/codegen.js'
import type { InputSchema } from '../src/codegen.js'
// Helper: eval the generated Worker code and call its fetch handler
async function callWorker(
code: string,
options: {
method?: string
url?: string
body?: unknown
searchParams?: Record<string, string>
} = {},
): Promise<Response> {
const { method = 'GET', url = 'https://example.com/', body, searchParams } = options
// Build URL with search params
const reqUrl = new URL(url)
if (searchParams) {
for (const [k, v] of Object.entries(searchParams)) {
reqUrl.searchParams.set(k, v)
}
}
const init: RequestInit = { method }
if (body !== undefined && (method === 'POST' || method === 'PUT')) {
init.body = JSON.stringify(body)
init.headers = { 'Content-Type': 'application/json' }
}
const request = new Request(reqUrl.toString(), init)
// Evaluate the worker code and get the default export
const module = await import(/* @vite-ignore */ `data:text/javascript,${encodeURIComponent(code)}`)
const worker = module.default
return worker.fetch(request)
}
describe('codegen: generateWorkerCode', () => {
// Test 1: 基本代码生成
it('schema + execute → 生成有效 Worker 代码', () => {
const schema: InputSchema = {
type: 'object',
properties: {
name: { type: 'string', description: 'Name to greet' },
},
}
const execute = `return "Hello, " + (input.name || "World") + "!"`
const code = generateWorkerCode(schema, execute)
expect(code).toContain('export default')
expect(code).toContain('async fetch(request)')
expect(code).toContain('input.name')
expect(typeof code).toBe('string')
})
// Test 2: 类型转换 — number/boolean 从 query string 正确转换
it('number 类型从 query string 正确转换', async () => {
const schema: InputSchema = {
type: 'object',
properties: {
amount: { type: 'number', description: 'Amount' },
},
}
const execute = `return JSON.stringify({ amount: input.amount, type: typeof input.amount })`
const code = generateWorkerCode(schema, execute)
const resp = await callWorker(code, { searchParams: { amount: '42.5' } })
expect(resp.status).toBe(200)
const data = await resp.json() as { amount: number; type: string }
expect(data.amount).toBe(42.5)
expect(data.type).toBe('number')
})
it('boolean 类型从 query string 正确转换', async () => {
const schema: InputSchema = {
type: 'object',
properties: {
flag: { type: 'boolean', description: 'A flag' },
},
}
const execute = `return JSON.stringify({ flag: input.flag, type: typeof input.flag })`
const code = generateWorkerCode(schema, execute)
const respTrue = await callWorker(code, { searchParams: { flag: 'true' } })
const dataTrue = await respTrue.json() as { flag: boolean; type: string }
expect(dataTrue.flag).toBe(true)
expect(dataTrue.type).toBe('boolean')
const respFalse = await callWorker(code, { searchParams: { flag: 'false' } })
const dataFalse = await respFalse.json() as { flag: boolean; type: string }
expect(dataFalse.flag).toBe(false)
})
// Test 3: 默认值填充
it('缺少参数时用默认值', async () => {
const schema: InputSchema = {
type: 'object',
properties: {
amount: { type: 'number', description: 'Amount', default: 1 },
currency: { type: 'string', description: 'Currency', default: 'USD' },
},
}
const execute = `return JSON.stringify({ amount: input.amount, currency: input.currency })`
const code = generateWorkerCode(schema, execute)
const resp = await callWorker(code)
expect(resp.status).toBe(200)
const data = await resp.json() as { amount: number; currency: string }
expect(data.amount).toBe(1)
expect(data.currency).toBe('USD')
})
// Test 4: required 校验 — 缺少必填参数返回 400
it('缺少 required 参数返回 400', async () => {
const schema: InputSchema = {
type: 'object',
properties: {
from: { type: 'string' },
to: { type: 'string' },
},
required: ['from', 'to'],
}
const execute = `return JSON.stringify({ from: input.from, to: input.to })`
const code = generateWorkerCode(schema, execute)
// Missing both required params
const resp1 = await callWorker(code)
expect(resp1.status).toBe(400)
const data1 = await resp1.json() as { error: string }
expect(data1.error).toContain('Missing required parameter: from')
// Only `from` provided
const resp2 = await callWorker(code, { searchParams: { from: 'USD' } })
expect(resp2.status).toBe(400)
const data2 = await resp2.json() as { error: string }
expect(data2.error).toContain('Missing required parameter: to')
// Both provided — should succeed
const resp3 = await callWorker(code, { searchParams: { from: 'USD', to: 'CNY' } })
expect(resp3.status).toBe(200)
})
// Test 5: 空 schema — 无参数的函数
it('空 schema — 无参数的函数正常运行', async () => {
const schema: InputSchema = { properties: {} }
const execute = `return "hello world"`
const code = generateWorkerCode(schema, execute)
const resp = await callWorker(code)
expect(resp.status).toBe(200)
const text = await resp.text()
expect(text).toBe('hello world')
})
// Test 6: POST body 解析 — JSON body 正确读取
it('POST body 解析 — JSON body 正确读取', async () => {
const schema: InputSchema = {
type: 'object',
properties: {
x: { type: 'number' },
y: { type: 'number' },
},
}
const execute = `return JSON.stringify({ sum: input.x + input.y })`
const code = generateWorkerCode(schema, execute)
const resp = await callWorker(code, {
method: 'POST',
body: { x: 10, y: 20 },
})
expect(resp.status).toBe(200)
const data = await resp.json() as { sum: number }
expect(data.sum).toBe(30)
})
// Test 7: 错误处理 — execute 抛错返回 500
it('execute 抛错返回 500', async () => {
const schema: InputSchema = { properties: {} }
const execute = `throw new Error("intentional error")`
const code = generateWorkerCode(schema, execute)
const resp = await callWorker(code)
expect(resp.status).toBe(500)
const data = await resp.json() as { error: string }
expect(data.error).toContain('intentional error')
})
// Test 8: query params override POST body
it('query params 覆盖 POST body 同名参数', async () => {
const schema: InputSchema = {
type: 'object',
properties: {
value: { type: 'string' },
},
}
const execute = `return input.value`
const code = generateWorkerCode(schema, execute)
const resp = await callWorker(code, {
method: 'POST',
url: 'https://example.com/?value=from-query',
body: { value: 'from-body' },
})
expect(resp.status).toBe(200)
const text = await resp.text()
// query params should override body
expect(text).toBe('from-query')
})
// Test 9: non-string output auto-stringified
it('非 string 返回值自动 JSON 序列化', async () => {
const schema: InputSchema = { properties: {} }
const execute = `return { hello: "world", num: 42 }`
const code = generateWorkerCode(schema, execute)
const resp = await callWorker(code)
expect(resp.status).toBe(200)
const data = await resp.json() as { hello: string; num: number }
expect(data.hello).toBe('world')
expect(data.num).toBe(42)
})
})