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:
2026-04-03 09:43:19 +00:00
parent ce4c2b7b36
commit 120e62d7e4
16 changed files with 189 additions and 165 deletions
+71 -74
View File
@@ -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
// 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 ?? 0) + 1,
access_count: lru.access_count + 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,
})
}
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,
}
+50 -15
View File
@@ -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
View File
@@ -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)
+3 -6
View File
@@ -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 ---
+4 -4
View File
@@ -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)
})
})
+12 -7
View File
@@ -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()
})
})
+2 -19
View File
@@ -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({
+1 -1
View File
@@ -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 () => {
+1 -1
View File
@@ -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)
+4 -4
View File
@@ -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)
+4 -4
View File
@@ -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)
+1 -1
View File
@@ -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)
})
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
})
+1 -1
View File
@@ -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)
+15 -8
View File
@@ -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,17 +93,18 @@ 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[] = []
return {
getCalls,
loader: {
async get(workerId: string, _loadFn: () => Promise<{ code: string }>) {
// 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() {
@@ -118,7 +119,13 @@ export function createMockLoader(overrides?: {
},
}
},
},
}
return {
getCalls,
/** The LOADER binding to pass to WorkerPool constructor */
loader: loaderBinding,
loaderCalls(): string[] {
return getCalls.map(c => c.workerId)