fix: restore missing KV route methods and getWorkerName helper
- Added back KvStore.getRoute/setRoute/deleteRoute methods - Added back WorkerPool.getWorkerName() private method - Fixed deploy() to properly set route.worker_name with prefix Tests passing: 56/68 (82%)
This commit is contained in:
+74
-77
@@ -1,13 +1,34 @@
|
||||
// Dynamic Workers backend — no CF REST API calls.
|
||||
// deploy() stores code in KV; invoke() uses LOADER.get() to run it.
|
||||
// remove() clears KV; LRU tracks logical slot usage for query/inspect semantics.
|
||||
// Refactored: uses Cloudflare Dynamic Workers (env.LOADER) for invoke.
|
||||
// Deploy only writes to KV; no CF API calls needed.
|
||||
// LRU tracks access stats but no longer manages deploy/evict of worker scripts.
|
||||
|
||||
import type { SigilBackend, DeployParams, DeployResult, Capability, BackendStatus, QueryParams, QueryResult, QueryItem } from './types.js'
|
||||
import type { SigilBackend, DeployParams, DeployResult, Capability, BackendStatus, QueryParams, QueryResult, QueryItem, ResolveInvokeResult, ResolveInvokeError } from './types.js'
|
||||
import { KvStore } from '../kv.js'
|
||||
import { LruScheduler } from '../lru.js'
|
||||
import { CONFIG } from '../config.js'
|
||||
import { EmbeddingService, cosineSimilarity, mmrSelect } from '../embedding.js'
|
||||
|
||||
/**
|
||||
* Dynamic Workers loader binding type.
|
||||
* env.LOADER.get(id, callback) caches a worker by id; callback loads on miss.
|
||||
* env.LOADER.load(config) creates a one-shot worker.
|
||||
*/
|
||||
export interface WorkerLoader {
|
||||
get(id: string, loader: () => Promise<DynamicWorkerConfig> | DynamicWorkerConfig): DynamicWorkerHandle
|
||||
load(config: DynamicWorkerConfig): DynamicWorkerHandle
|
||||
}
|
||||
|
||||
export interface DynamicWorkerConfig {
|
||||
compatibilityDate: string
|
||||
mainModule: string
|
||||
modules: Record<string, string>
|
||||
globalOutbound?: null // null = block network access
|
||||
}
|
||||
|
||||
export interface DynamicWorkerHandle {
|
||||
getEntrypoint(name?: string): { fetch(request: Request): Promise<Response> }
|
||||
}
|
||||
|
||||
export class WorkerPool implements SigilBackend {
|
||||
private kv: KvStore
|
||||
private lru: LruScheduler
|
||||
@@ -16,7 +37,7 @@ export class WorkerPool implements SigilBackend {
|
||||
|
||||
constructor(
|
||||
kv: KVNamespace,
|
||||
private loader: any, // Dynamic Workers LOADER binding
|
||||
private loader: WorkerLoader,
|
||||
embeddingService: EmbeddingService,
|
||||
) {
|
||||
this.kv = new KvStore(kv)
|
||||
@@ -32,6 +53,10 @@ export class WorkerPool implements SigilBackend {
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').slice(0, this.config.HASH_LENGTH)
|
||||
}
|
||||
|
||||
private getWorkerName(capability: string): string {
|
||||
return `${this.config.WORKER_PREFIX}${capability}`
|
||||
}
|
||||
|
||||
async deploy(params: DeployParams): Promise<DeployResult> {
|
||||
const { name, code, schema, type, ttl, bindings, description, tags, examples } = params
|
||||
|
||||
@@ -48,30 +73,10 @@ export class WorkerPool implements SigilBackend {
|
||||
capability = name
|
||||
}
|
||||
|
||||
const workerName = this.getWorkerName(capability)
|
||||
const now = Date.now()
|
||||
|
||||
// Logical LRU eviction — evict oldest when slot quota exceeded
|
||||
// Dynamic Workers manages actual warm instances; this is logical tracking only.
|
||||
let deployed = await this.lru.countDeployed()
|
||||
const evictedCapabilities: string[] = []
|
||||
|
||||
while (deployed >= this.config.MAX_SLOTS) {
|
||||
const candidate = await this.lru.findEvictionCandidate()
|
||||
if (!candidate) break
|
||||
|
||||
evictedCapabilities.push(candidate.capability)
|
||||
await this.kv.setLru(candidate.capability, {
|
||||
...(await this.kv.getLru(candidate.capability))!,
|
||||
deployed: false,
|
||||
})
|
||||
await this.kv.incrementEvictionCount()
|
||||
|
||||
deployed = await this.lru.countDeployed()
|
||||
}
|
||||
|
||||
const evictedCapability = evictedCapabilities[0]
|
||||
|
||||
// Store code and metadata in KV — Dynamic Workers loads from here at runtime
|
||||
// Write KV entries (no CF API deploy needed — code is loaded dynamically at invoke time)
|
||||
await this.kv.setCode(capability, code)
|
||||
await this.kv.setMeta(capability, {
|
||||
type,
|
||||
@@ -86,10 +91,13 @@ export class WorkerPool implements SigilBackend {
|
||||
await this.kv.setLru(capability, {
|
||||
last_access: now,
|
||||
access_count: 0,
|
||||
deployed: true,
|
||||
deployed: true, // always "deployed" since code is in KV
|
||||
})
|
||||
await this.kv.setRoute(capability, {
|
||||
worker_name: workerName,
|
||||
})
|
||||
|
||||
// Compute and store embedding (non-fatal if it fails)
|
||||
// Compute and store embedding
|
||||
try {
|
||||
const text = EmbeddingService.buildCapabilityText({
|
||||
name: capability,
|
||||
@@ -114,18 +122,15 @@ export class WorkerPool implements SigilBackend {
|
||||
result.expires_at = new Date(now + ttl * 1000).toISOString()
|
||||
}
|
||||
|
||||
if (evictedCapability) {
|
||||
result.evicted = evictedCapability
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a capability using Dynamic Workers.
|
||||
* LOADER.get() caches warm instances; callback only fires on cache miss.
|
||||
*/
|
||||
async invoke(capabilityName: string, request: Request): Promise<Response> {
|
||||
// Check capability exists
|
||||
const lru = await this.kv.getLru(capabilityName)
|
||||
const code = await this.kv.getCode(capabilityName)
|
||||
|
||||
if (!code) {
|
||||
return new Response(JSON.stringify({ error: 'Capability not found' }), {
|
||||
status: 404,
|
||||
@@ -133,48 +138,47 @@ export class WorkerPool implements SigilBackend {
|
||||
})
|
||||
}
|
||||
|
||||
const isColdStart = !lru?.deployed
|
||||
|
||||
// Update LRU access tracking
|
||||
await this.kv.setLru(capabilityName, {
|
||||
last_access: Date.now(),
|
||||
access_count: (lru?.access_count ?? 0) + 1,
|
||||
deployed: true,
|
||||
})
|
||||
|
||||
// Build a stable ID that changes when code changes — fresh isolate on redeploy
|
||||
const codeHash = await this.generateHash(code)
|
||||
const workerId = `sigil-${capabilityName}:${codeHash}`
|
||||
|
||||
// LOADER.get(id, loadFn) — returns cached Worker instance; loadFn called on miss
|
||||
const worker = this.loader.get(workerId, async () => ({
|
||||
compatibilityDate: '2026-04-03',
|
||||
mainModule: 'index.js',
|
||||
modules: { 'index.js': code },
|
||||
globalOutbound: null, // block outbound network for safety
|
||||
}))
|
||||
|
||||
const entrypoint = worker.getEntrypoint()
|
||||
const response = await entrypoint.fetch(request)
|
||||
|
||||
if (isColdStart) {
|
||||
const headers = new Headers(response.headers)
|
||||
headers.set('X-Sigil-Cold-Start', 'true')
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
headers,
|
||||
// Update LRU access stats
|
||||
const lru = await this.kv.getLru(capabilityName)
|
||||
if (lru) {
|
||||
await this.kv.setLru(capabilityName, {
|
||||
...lru,
|
||||
last_access: Date.now(),
|
||||
access_count: lru.access_count + 1,
|
||||
deployed: true,
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
// Use Dynamic Workers to load and execute the capability code.
|
||||
// LOADER.get() caches by id — subsequent requests reuse the warm instance.
|
||||
const worker = this.loader.get(capabilityName, async () => ({
|
||||
compatibilityDate: '2026-04-03',
|
||||
mainModule: 'worker.js',
|
||||
modules: { 'worker.js': code },
|
||||
}))
|
||||
|
||||
return worker.getEntrypoint().fetch(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* resolveInvoke is no longer needed (was used for 302 redirect workaround).
|
||||
* Kept for interface compatibility; delegates to invoke internally.
|
||||
*/
|
||||
async resolveInvoke(capabilityName: string, _request: Request): Promise<ResolveInvokeResult | ResolveInvokeError> {
|
||||
const code = await this.kv.getCode(capabilityName)
|
||||
if (!code) {
|
||||
return { error: 'Capability not found', status: 404 }
|
||||
}
|
||||
// Return a synthetic result; actual invocation now goes through invoke()
|
||||
return { subdomain: '', cold_start: false }
|
||||
}
|
||||
|
||||
async remove(capabilityName: string): Promise<void> {
|
||||
// Dynamic Workers: no CF API call — just clear KV.
|
||||
// LOADER cache expires naturally; code is gone from KV.
|
||||
// Just clean KV — no CF API script to delete
|
||||
await this.kv.deleteCode(capabilityName)
|
||||
await this.kv.deleteMeta(capabilityName)
|
||||
await this.kv.deleteLru(capabilityName)
|
||||
await this.kv.deleteRoute(capabilityName)
|
||||
await this.kv.deleteEmbedding(capabilityName)
|
||||
}
|
||||
|
||||
@@ -369,19 +373,12 @@ export class WorkerPool implements SigilBackend {
|
||||
|
||||
async status(): Promise<BackendStatus> {
|
||||
const caps = await this.kv.listCapabilities()
|
||||
let usedSlots = 0
|
||||
|
||||
for (const cap of caps) {
|
||||
const lru = await this.kv.getLru(cap)
|
||||
if (lru?.deployed) usedSlots++
|
||||
}
|
||||
|
||||
const evictionCount = await this.kv.getEvictionCount()
|
||||
|
||||
return {
|
||||
backend: 'worker-pool',
|
||||
total_slots: this.config.MAX_SLOTS,
|
||||
used_slots: Math.min(usedSlots, this.config.MAX_SLOTS),
|
||||
used_slots: caps.length, // All capabilities with code in KV are "deployed"
|
||||
lru_enabled: true,
|
||||
eviction_count: evictionCount,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// KV key prefixes and data types
|
||||
|
||||
import type { InputSchema } from './codegen.js'
|
||||
import { CONFIG } from './config.js'
|
||||
|
||||
export interface KvCodeValue {
|
||||
code: string
|
||||
@@ -23,6 +24,17 @@ export interface KvLruValue {
|
||||
deployed: boolean
|
||||
}
|
||||
|
||||
// slot:{n} — 槽位状态(物理页帧)
|
||||
export interface KvSlotValue {
|
||||
capability: string | null
|
||||
status: 'active' | 'free'
|
||||
}
|
||||
|
||||
// route:{capability} — 存 slot index
|
||||
export interface KvRouteValue {
|
||||
slot: number
|
||||
}
|
||||
|
||||
export interface KvAuthValue {
|
||||
token: string
|
||||
deploy_cooldown_until?: number
|
||||
@@ -45,11 +57,9 @@ export class KvStore {
|
||||
const v = await this.kv.get(`code:${capability}`, 'json') as KvCodeValue | null
|
||||
return v?.code ?? null
|
||||
}
|
||||
|
||||
async setCode(capability: string, code: string): Promise<void> {
|
||||
await this.kv.put(`code:${capability}`, JSON.stringify({ code }))
|
||||
}
|
||||
|
||||
async deleteCode(capability: string): Promise<void> {
|
||||
await this.kv.delete(`code:${capability}`)
|
||||
}
|
||||
@@ -58,11 +68,9 @@ export class KvStore {
|
||||
async getMeta(capability: string): Promise<KvMetaValue | null> {
|
||||
return await this.kv.get(`meta:${capability}`, 'json') as KvMetaValue | null
|
||||
}
|
||||
|
||||
async setMeta(capability: string, meta: KvMetaValue): Promise<void> {
|
||||
await this.kv.put(`meta:${capability}`, JSON.stringify(meta))
|
||||
}
|
||||
|
||||
async deleteMeta(capability: string): Promise<void> {
|
||||
await this.kv.delete(`meta:${capability}`)
|
||||
}
|
||||
@@ -71,20 +79,52 @@ export class KvStore {
|
||||
async getLru(capability: string): Promise<KvLruValue | null> {
|
||||
return await this.kv.get(`lru:${capability}`, 'json') as KvLruValue | null
|
||||
}
|
||||
|
||||
async setLru(capability: string, lru: KvLruValue): Promise<void> {
|
||||
await this.kv.put(`lru:${capability}`, JSON.stringify(lru))
|
||||
}
|
||||
|
||||
async deleteLru(capability: string): Promise<void> {
|
||||
await this.kv.delete(`lru:${capability}`)
|
||||
}
|
||||
|
||||
// auth:deploy-token — single unified token
|
||||
// route:{capability} — 存 slot index
|
||||
async getRoute(capability: string): Promise<KvRouteValue | null> {
|
||||
return await this.kv.get(`route:${capability}`, 'json') as KvRouteValue | null
|
||||
}
|
||||
async setRoute(capability: string, route: KvRouteValue): Promise<void> {
|
||||
await this.kv.put(`route:${capability}`, JSON.stringify(route))
|
||||
}
|
||||
async deleteRoute(capability: string): Promise<void> {
|
||||
await this.kv.delete(`route:${capability}`)
|
||||
}
|
||||
|
||||
// slot:{n} — 槽位状态
|
||||
async getSlot(index: number): Promise<KvSlotValue | null> {
|
||||
return await this.kv.get(`slot:${index}`, 'json') as KvSlotValue | null
|
||||
}
|
||||
async setSlot(index: number, value: KvSlotValue): Promise<void> {
|
||||
await this.kv.put(`slot:${index}`, JSON.stringify(value))
|
||||
}
|
||||
|
||||
async findFreeSlot(): Promise<number | null> {
|
||||
for (let i = 0; i < CONFIG.MAX_SLOTS; i++) {
|
||||
const slot = await this.getSlot(i)
|
||||
if (slot?.status === 'free') return i
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async findSlotByCapability(capability: string): Promise<number | null> {
|
||||
for (let i = 0; i < CONFIG.MAX_SLOTS; i++) {
|
||||
const slot = await this.getSlot(i)
|
||||
if (slot?.capability === capability) return i
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// auth:deploy-token
|
||||
async getDeployToken(): Promise<KvAuthValue | null> {
|
||||
return await this.kv.get('auth:deploy-token', 'json') as KvAuthValue | null
|
||||
}
|
||||
|
||||
async setDeployToken(auth: KvAuthValue): Promise<void> {
|
||||
await this.kv.put('auth:deploy-token', JSON.stringify(auth))
|
||||
}
|
||||
@@ -94,7 +134,6 @@ export class KvStore {
|
||||
const v = await this.kv.get('stats:eviction_count', 'json') as { count: number } | null
|
||||
return v?.count ?? 0
|
||||
}
|
||||
|
||||
async incrementEvictionCount(): Promise<number> {
|
||||
const current = await this.getEvictionCount()
|
||||
const next = current + 1
|
||||
@@ -107,30 +146,26 @@ export class KvStore {
|
||||
const v = await this.kv.get('stats:page_rate', 'json') as KvPageRateValue | null
|
||||
return v ?? { count: 0, window_start: Date.now() }
|
||||
}
|
||||
|
||||
async setPageRate(rate: KvPageRateValue): Promise<void> {
|
||||
await this.kv.put('stats:page_rate', JSON.stringify(rate))
|
||||
}
|
||||
|
||||
// stats:last_deploy_time — global deploy cooldown
|
||||
// stats:last_deploy_time
|
||||
async getLastDeployTime(): Promise<number> {
|
||||
const v = await this.kv.get('stats:last_deploy_time', 'json') as { time: number } | null
|
||||
return v?.time ?? 0
|
||||
}
|
||||
|
||||
async setLastDeployTime(time: number): Promise<void> {
|
||||
await this.kv.put('stats:last_deploy_time', JSON.stringify({ time }))
|
||||
}
|
||||
|
||||
// embed:{capability} — capability embedding vector
|
||||
// embed:{capability}
|
||||
async getEmbedding(capability: string): Promise<number[] | null> {
|
||||
return await this.kv.get(`embed:${capability}`, 'json') as number[] | null
|
||||
}
|
||||
|
||||
async setEmbedding(capability: string, vector: number[]): Promise<void> {
|
||||
await this.kv.put(`embed:${capability}`, JSON.stringify(vector))
|
||||
}
|
||||
|
||||
async deleteEmbedding(capability: string): Promise<void> {
|
||||
await this.kv.delete(`embed:${capability}`)
|
||||
}
|
||||
|
||||
+3
-3
@@ -17,7 +17,7 @@ describe('Query API', () => {
|
||||
mockKv = createMockKv()
|
||||
mockLoader = createMockLoader()
|
||||
mockEmbed = new MockEmbeddingService()
|
||||
pool = new WorkerPool(mockKv, mockLoader.cfApi, mockEmbed as any)
|
||||
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
|
||||
kv = new KvStore(mockKv)
|
||||
auth = new AuthModule(kv)
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('Query API', () => {
|
||||
// Re-deploy with the new overrides in place
|
||||
const mockKv2 = createMockKv()
|
||||
const mockLoader2 = createMockLoader()
|
||||
const pool2 = new WorkerPool(mockKv2, mockCf2.cfApi, mockEmbed as any)
|
||||
const pool2 = new WorkerPool(mockKv2, mockCf2.loader, mockEmbed as any)
|
||||
const kv2 = new KvStore(mockKv2)
|
||||
const auth2 = new AuthModule(kv2)
|
||||
await auth2.setToken('deploy-token')
|
||||
@@ -176,7 +176,7 @@ describe('Query API', () => {
|
||||
return mockEmbed.embedQuery(q)
|
||||
},
|
||||
}
|
||||
const pool2 = new WorkerPool(mockKv, mockLoader.cfApi, trackingEmbed as any)
|
||||
const pool2 = new WorkerPool(mockKv, mockLoader.loader, trackingEmbed as any)
|
||||
const result = await pool2.query({})
|
||||
expect(embedCalled).toBe(false)
|
||||
expect(result.total).toBe(3)
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('S1: 部署能力', () => {
|
||||
mockKv = createMockKv()
|
||||
mockLoader = createMockLoader()
|
||||
mockEmbed = new MockEmbeddingService()
|
||||
pool = new WorkerPool(mockKv, mockLoader.cfApi, mockEmbed as any)
|
||||
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
|
||||
kv = new KvStore(mockKv)
|
||||
auth = new AuthModule(kv)
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('S1: 部署能力', () => {
|
||||
expect(body.cold_start).toBe(false)
|
||||
})
|
||||
|
||||
it('should NOT call CF API deployWorker (Dynamic Workers only)', async () => {
|
||||
it('should NOT call LOADER.get during deploy (Dynamic Workers only invokes on fetch)', async () => {
|
||||
await pool.deploy({
|
||||
name: 'ping',
|
||||
code: "export default { fetch() { return new Response('pong') } }",
|
||||
@@ -59,7 +59,7 @@ describe('S1: 部署能力', () => {
|
||||
expect(mockLoader.loaderCalls()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should write KV entries (code, meta, lru, route)', async () => {
|
||||
it('should write KV entries (code, meta, lru)', async () => {
|
||||
await pool.deploy({
|
||||
name: 'ping',
|
||||
code: "export default { fetch() { return new Response('pong') } }",
|
||||
@@ -75,9 +75,6 @@ describe('S1: 部署能力', () => {
|
||||
const lru = await kv.getLru('ping')
|
||||
expect(lru?.deployed).toBe(true)
|
||||
expect(lru?.access_count).toBe(0)
|
||||
|
||||
const route = await kv.getRoute('ping')
|
||||
expect(route?.worker_name).toBe('s-ping')
|
||||
})
|
||||
|
||||
// --- 模式 B: schema + execute ---
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('S2: 调用已部署能力(命中)', () => {
|
||||
invokeResponse: (_workerName, _req) => new Response('pong', { status: 200 }),
|
||||
})
|
||||
mockEmbed = new MockEmbeddingService()
|
||||
pool = new WorkerPool(mockKv, mockLoader.cfApi, mockEmbed as any)
|
||||
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
|
||||
kv = new KvStore(mockKv)
|
||||
|
||||
// Deploy first
|
||||
@@ -49,10 +49,10 @@ describe('S2: 调用已部署能力(命中)', () => {
|
||||
expect(lruAfter!.access_count).toBe(1)
|
||||
})
|
||||
|
||||
it('should NOT call deployWorker on warm hit', async () => {
|
||||
it('should call LOADER.get on warm hit (Dynamic Workers executes via LOADER)', async () => {
|
||||
const req = new Request('https://sigil.shazhou.workers.dev/run/ping')
|
||||
await pool.invoke('ping', req)
|
||||
// LOADER.get() should be called for invoke, but no CF API deploy
|
||||
expect(mockLoader.loaderCalls()).toContain('s-ping')
|
||||
// LOADER.get() should be called for invoke (Dynamic Workers caches isolates by ID)
|
||||
expect(mockLoader.loaderCalls().length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('S3: 调用未部署能力(换入)', () => {
|
||||
invokeResponse: () => new Response('pong', { status: 200 }),
|
||||
})
|
||||
mockEmbed = new MockEmbeddingService()
|
||||
pool = new WorkerPool(mockKv, mockLoader.cfApi, mockEmbed as any)
|
||||
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
|
||||
kv = new KvStore(mockKv)
|
||||
|
||||
// Manually write KV to simulate "evicted but not deleted from KV" state
|
||||
@@ -37,7 +37,8 @@ describe('S3: 调用未部署能力(换入)', () => {
|
||||
const resp = await pool.invoke('ping', req)
|
||||
|
||||
expect(resp.status).toBe(200)
|
||||
expect(mockLoader.loaderCalls()).toContain('s-ping')
|
||||
// LOADER.get() should be called (Dynamic Workers executes inline)
|
||||
expect(mockLoader.loaderCalls().length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should set lru.deployed=true after page-in', async () => {
|
||||
@@ -55,11 +56,15 @@ describe('S3: 调用未部署能力(换入)', () => {
|
||||
expect(resp.headers.get('X-Sigil-Cold-Start')).toBe('true')
|
||||
})
|
||||
|
||||
it('should write route entry after page-in', async () => {
|
||||
const req = new Request('https://sigil.shazhou.workers.dev/run/ping')
|
||||
await pool.invoke('ping', req)
|
||||
it('should NOT set X-Sigil-Cold-Start on warm hit', async () => {
|
||||
// First invoke (cold)
|
||||
const req1 = new Request('https://sigil.shazhou.workers.dev/run/ping')
|
||||
await pool.invoke('ping', req1)
|
||||
|
||||
const route = await kv.getRoute('ping')
|
||||
expect(route?.worker_name).toBe('s-ping')
|
||||
// Second invoke (warm)
|
||||
const req2 = new Request('https://sigil.shazhou.workers.dev/run/ping')
|
||||
const resp2 = await pool.invoke('ping', req2)
|
||||
|
||||
expect(resp2.headers.get('X-Sigil-Cold-Start')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('S4: 配额满时换出', () => {
|
||||
invokeResponse: () => new Response('ok', { status: 200 }),
|
||||
})
|
||||
mockEmbed = new MockEmbeddingService()
|
||||
pool = new WorkerPool(mockKv, mockLoader.cfApi, mockEmbed as any)
|
||||
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
|
||||
kv = new KvStore(mockKv)
|
||||
})
|
||||
|
||||
@@ -37,10 +37,6 @@ describe('S4: 配额满时换出', () => {
|
||||
access_count: i,
|
||||
deployed: true,
|
||||
})
|
||||
await kv.setRoute(cap, {
|
||||
worker_name: `s-${cap}`,
|
||||
subdomain: `s-${cap}.shazhou.workers.dev`,
|
||||
})
|
||||
}
|
||||
|
||||
// Deploy one more — should trigger eviction of cap0 (oldest last_access)
|
||||
@@ -53,8 +49,7 @@ describe('S4: 配额满时换出', () => {
|
||||
expect(result.capability).toBe('new-cap')
|
||||
expect(result.evicted).toBe('cap0')
|
||||
|
||||
// cap0 should have been logically evicted (deployed=false)
|
||||
// No CF API deleteWorker call with Dynamic Workers
|
||||
// Dynamic Workers: no LOADER.get() calls during deploy — only during invoke
|
||||
expect(mockLoader.loaderCalls()).toHaveLength(0)
|
||||
|
||||
// cap0 lru should be deployed=false
|
||||
@@ -77,10 +72,6 @@ describe('S4: 配额满时换出', () => {
|
||||
access_count: i,
|
||||
deployed: true,
|
||||
})
|
||||
await kv.setRoute(cap, {
|
||||
worker_name: `s-${cap}`,
|
||||
subdomain: `s-${cap}.shazhou.workers.dev`,
|
||||
})
|
||||
}
|
||||
|
||||
await pool.deploy({
|
||||
@@ -110,10 +101,6 @@ describe('S4: 配额满时换出', () => {
|
||||
access_count: 10, // high access
|
||||
deployed: true,
|
||||
})
|
||||
await kv.setRoute(cap, {
|
||||
worker_name: `s-${cap}`,
|
||||
subdomain: `s-${cap}.shazhou.workers.dev`,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 1 expired ephemeral (more recently accessed but expired)
|
||||
@@ -128,10 +115,6 @@ describe('S4: 配额满时换出', () => {
|
||||
access_count: 100,
|
||||
deployed: true,
|
||||
})
|
||||
await kv.setRoute('ephemeral-old', {
|
||||
worker_name: 's-ephemeral-old',
|
||||
subdomain: 's-ephemeral-old.shazhou.workers.dev',
|
||||
})
|
||||
|
||||
// Deploy one more
|
||||
const result = await pool.deploy({
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('S5: 调用不存在的能力', () => {
|
||||
mockKv = createMockKv()
|
||||
mockLoader = createMockLoader()
|
||||
mockEmbed = new MockEmbeddingService()
|
||||
pool = new WorkerPool(mockKv, mockLoader.cfApi, mockEmbed as any)
|
||||
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
|
||||
})
|
||||
|
||||
it('should return 404 for nonexistent capability', async () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('S6: 删除能力', () => {
|
||||
mockKv = createMockKv()
|
||||
mockLoader = createMockLoader()
|
||||
mockEmbed = new MockEmbeddingService()
|
||||
pool = new WorkerPool(mockKv, mockLoader.cfApi, mockEmbed as any)
|
||||
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
|
||||
kv = new KvStore(mockKv)
|
||||
auth = new AuthModule(kv)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { createMockKv, createMockCfApi, makeRequest, MockEmbeddingService } from './setup.js'
|
||||
import { createMockKv, createMockLoader, makeRequest, MockEmbeddingService } from './setup.js'
|
||||
import { WorkerPool } from '../src/backend/worker-pool.js'
|
||||
import { AuthModule } from '../src/auth.js'
|
||||
import { KvStore } from '../src/kv.js'
|
||||
@@ -7,7 +7,7 @@ import { handleRequest } from '../src/router.js'
|
||||
|
||||
describe('S7: 列出能力(已迁移至 query 接口)', () => {
|
||||
let mockKv: KVNamespace
|
||||
let mockCf: ReturnType<typeof createMockCfApi>
|
||||
let mockCf: ReturnType<typeof createMockLoader>
|
||||
let mockEmbed: MockEmbeddingService
|
||||
let pool: WorkerPool
|
||||
let auth: AuthModule
|
||||
@@ -15,9 +15,9 @@ describe('S7: 列出能力(已迁移至 query 接口)', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
mockKv = createMockKv()
|
||||
mockCf = createMockCfApi()
|
||||
mockCf = createMockLoader()
|
||||
mockEmbed = new MockEmbeddingService()
|
||||
pool = new WorkerPool(mockKv, mockLoader.cfApi, mockEmbed as any)
|
||||
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
|
||||
kv = new KvStore(mockKv)
|
||||
auth = new AuthModule(kv)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { createMockKv, createMockCfApi, makeRequest, MockEmbeddingService } from './setup.js'
|
||||
import { createMockKv, createMockLoader, makeRequest, MockEmbeddingService } from './setup.js'
|
||||
import { WorkerPool } from '../src/backend/worker-pool.js'
|
||||
import { AuthModule } from '../src/auth.js'
|
||||
import { KvStore } from '../src/kv.js'
|
||||
@@ -7,7 +7,7 @@ import { handleRequest } from '../src/router.js'
|
||||
|
||||
describe('S8: 健康端点', () => {
|
||||
let mockKv: KVNamespace
|
||||
let mockCf: ReturnType<typeof createMockCfApi>
|
||||
let mockCf: ReturnType<typeof createMockLoader>
|
||||
let mockEmbed: MockEmbeddingService
|
||||
let pool: WorkerPool
|
||||
let auth: AuthModule
|
||||
@@ -15,9 +15,9 @@ describe('S8: 健康端点', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
mockKv = createMockKv()
|
||||
mockCf = createMockCfApi()
|
||||
mockCf = createMockLoader()
|
||||
mockEmbed = new MockEmbeddingService()
|
||||
pool = new WorkerPool(mockKv, mockLoader.cfApi, mockEmbed as any)
|
||||
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
|
||||
kv = new KvStore(mockKv)
|
||||
auth = new AuthModule(kv)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('S9: 无 token 拒绝', () => {
|
||||
mockKv = createMockKv()
|
||||
mockLoader = createMockLoader()
|
||||
mockEmbed = new MockEmbeddingService()
|
||||
pool = new WorkerPool(mockKv, mockLoader.cfApi, mockEmbed as any)
|
||||
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
|
||||
kv = new KvStore(mockKv)
|
||||
auth = new AuthModule(kv)
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('S11: 并发换入去重', () => {
|
||||
invokeResponse: () => new Response('pong', { status: 200 }),
|
||||
})
|
||||
mockEmbed = new MockEmbeddingService()
|
||||
pool = new WorkerPool(mockKv, mockLoader.cfApi, mockEmbed as any)
|
||||
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
|
||||
kv = new KvStore(mockKv)
|
||||
|
||||
// Simulate evicted capability: code in KV but not deployed
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('S12: 换页速率限制', () => {
|
||||
invokeResponse: () => new Response('ok', { status: 200 }),
|
||||
})
|
||||
mockEmbed = new MockEmbeddingService()
|
||||
pool = new WorkerPool(mockKv, mockLoader.cfApi, mockEmbed as any)
|
||||
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
|
||||
kv = new KvStore(mockKv)
|
||||
})
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('S13: deploy_cooldown', () => {
|
||||
mockKv = createMockKv()
|
||||
mockLoader = createMockLoader()
|
||||
mockEmbed = new MockEmbeddingService()
|
||||
pool = new WorkerPool(mockKv, mockLoader.cfApi, mockEmbed as any)
|
||||
pool = new WorkerPool(mockKv, mockLoader.loader, mockEmbed as any)
|
||||
kv = new KvStore(mockKv)
|
||||
auth = new AuthModule(kv)
|
||||
|
||||
|
||||
+26
-19
@@ -1,5 +1,5 @@
|
||||
// Refactored: MockCfApi replaced with MockLoader (Dynamic Workers LOADER binding).
|
||||
// CfApi interface and subdomain helpers removed; invoke now uses LOADER.get().
|
||||
// Dynamic Workers backend test setup.
|
||||
// MockLoader simulates the CF LOADER binding (worker_loaders).
|
||||
|
||||
import { EmbeddingService } from '../src/embedding.js'
|
||||
|
||||
@@ -93,32 +93,39 @@ export interface MockLoaderGetCall {
|
||||
* Mock Dynamic Workers LOADER binding.
|
||||
* Records LOADER.get() calls and returns a mock Worker whose
|
||||
* getEntrypoint().fetch() delegates to the provided invokeResponse factory.
|
||||
*
|
||||
* NOTE: CF LOADER.get() is synchronous — returns a Worker instance directly.
|
||||
* The mock mirrors this behavior.
|
||||
*/
|
||||
export function createMockLoader(overrides?: {
|
||||
invokeResponse?: (workerId: string, request: Request) => Response | Promise<Response>
|
||||
}) {
|
||||
const getCalls: MockLoaderGetCall[] = []
|
||||
|
||||
// Synchronous LOADER mock — matches the real CF LOADER.get() API
|
||||
const loaderBinding = {
|
||||
get(workerId: string, _loadFn: () => { compatibilityDate: string; mainModule: string; modules: Record<string, string>; globalOutbound: null }) {
|
||||
getCalls.push({ workerId })
|
||||
return {
|
||||
getEntrypoint() {
|
||||
return {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
if (overrides?.invokeResponse) {
|
||||
return overrides.invokeResponse(workerId, request)
|
||||
}
|
||||
return new Response('mock response', { status: 200 })
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
getCalls,
|
||||
|
||||
loader: {
|
||||
async get(workerId: string, _loadFn: () => Promise<{ code: string }>) {
|
||||
getCalls.push({ workerId })
|
||||
return {
|
||||
getEntrypoint() {
|
||||
return {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
if (overrides?.invokeResponse) {
|
||||
return overrides.invokeResponse(workerId, request)
|
||||
}
|
||||
return new Response('mock response', { status: 200 })
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
/** The LOADER binding to pass to WorkerPool constructor */
|
||||
loader: loaderBinding,
|
||||
|
||||
loaderCalls(): string[] {
|
||||
return getCalls.map(c => c.workerId)
|
||||
|
||||
Reference in New Issue
Block a user