diff --git a/src/backend/worker-pool.ts b/src/backend/worker-pool.ts index 4833265..210ab71 100644 --- a/src/backend/worker-pool.ts +++ b/src/backend/worker-pool.ts @@ -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): DynamicWorkerHandle + load(config: DynamicWorkerConfig): DynamicWorkerHandle +} + +export interface DynamicWorkerConfig { + compatibilityDate: string + mainModule: string + modules: Record + globalOutbound?: null // null = block network access +} + +export interface DynamicWorkerHandle { + getEntrypoint(name?: string): { fetch(request: Request): Promise } +} + 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 { 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 { - // 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 { + 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 { - // 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 { 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, } diff --git a/src/kv.ts b/src/kv.ts index 5cb2178..22d10e9 100644 --- a/src/kv.ts +++ b/src/kv.ts @@ -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 { await this.kv.put(`code:${capability}`, JSON.stringify({ code })) } - async deleteCode(capability: string): Promise { await this.kv.delete(`code:${capability}`) } @@ -58,11 +68,9 @@ export class KvStore { async getMeta(capability: string): Promise { return await this.kv.get(`meta:${capability}`, 'json') as KvMetaValue | null } - async setMeta(capability: string, meta: KvMetaValue): Promise { await this.kv.put(`meta:${capability}`, JSON.stringify(meta)) } - async deleteMeta(capability: string): Promise { await this.kv.delete(`meta:${capability}`) } @@ -71,20 +79,52 @@ export class KvStore { async getLru(capability: string): Promise { return await this.kv.get(`lru:${capability}`, 'json') as KvLruValue | null } - async setLru(capability: string, lru: KvLruValue): Promise { await this.kv.put(`lru:${capability}`, JSON.stringify(lru)) } - async deleteLru(capability: string): Promise { await this.kv.delete(`lru:${capability}`) } - // auth:deploy-token — single unified token + // route:{capability} — 存 slot index + async getRoute(capability: string): Promise { + return await this.kv.get(`route:${capability}`, 'json') as KvRouteValue | null + } + async setRoute(capability: string, route: KvRouteValue): Promise { + await this.kv.put(`route:${capability}`, JSON.stringify(route)) + } + async deleteRoute(capability: string): Promise { + await this.kv.delete(`route:${capability}`) + } + + // slot:{n} — 槽位状态 + async getSlot(index: number): Promise { + return await this.kv.get(`slot:${index}`, 'json') as KvSlotValue | null + } + async setSlot(index: number, value: KvSlotValue): Promise { + await this.kv.put(`slot:${index}`, JSON.stringify(value)) + } + + async findFreeSlot(): Promise { + 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 { + 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 { return await this.kv.get('auth:deploy-token', 'json') as KvAuthValue | null } - async setDeployToken(auth: KvAuthValue): Promise { 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 { 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 { await this.kv.put('stats:page_rate', JSON.stringify(rate)) } - // stats:last_deploy_time — global deploy cooldown + // stats:last_deploy_time async getLastDeployTime(): Promise { const v = await this.kv.get('stats:last_deploy_time', 'json') as { time: number } | null return v?.time ?? 0 } - async setLastDeployTime(time: number): Promise { await this.kv.put('stats:last_deploy_time', JSON.stringify({ time })) } - // embed:{capability} — capability embedding vector + // embed:{capability} async getEmbedding(capability: string): Promise { return await this.kv.get(`embed:${capability}`, 'json') as number[] | null } - async setEmbedding(capability: string, vector: number[]): Promise { await this.kv.put(`embed:${capability}`, JSON.stringify(vector)) } - async deleteEmbedding(capability: string): Promise { await this.kv.delete(`embed:${capability}`) } diff --git a/test/query.test.ts b/test/query.test.ts index 8e70a9b..63844c0 100644 --- a/test/query.test.ts +++ b/test/query.test.ts @@ -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) diff --git a/test/s01-deploy.test.ts b/test/s01-deploy.test.ts index 9a30cf5..d6369d9 100644 --- a/test/s01-deploy.test.ts +++ b/test/s01-deploy.test.ts @@ -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 --- diff --git a/test/s02-invoke-hit.test.ts b/test/s02-invoke-hit.test.ts index 2181673..d91e4ce 100644 --- a/test/s02-invoke-hit.test.ts +++ b/test/s02-invoke-hit.test.ts @@ -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) }) }) diff --git a/test/s03-invoke-miss.test.ts b/test/s03-invoke-miss.test.ts index 4fa4272..730cda0 100644 --- a/test/s03-invoke-miss.test.ts +++ b/test/s03-invoke-miss.test.ts @@ -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() }) }) diff --git a/test/s04-eviction.test.ts b/test/s04-eviction.test.ts index 88e3900..9bd3ed4 100644 --- a/test/s04-eviction.test.ts +++ b/test/s04-eviction.test.ts @@ -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({ diff --git a/test/s05-not-found.test.ts b/test/s05-not-found.test.ts index e53827f..911829d 100644 --- a/test/s05-not-found.test.ts +++ b/test/s05-not-found.test.ts @@ -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 () => { diff --git a/test/s06-remove.test.ts b/test/s06-remove.test.ts index ee71dbb..171b301 100644 --- a/test/s06-remove.test.ts +++ b/test/s06-remove.test.ts @@ -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) diff --git a/test/s07-list.test.ts b/test/s07-list.test.ts index 7f24bb4..614d994 100644 --- a/test/s07-list.test.ts +++ b/test/s07-list.test.ts @@ -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 + let mockCf: ReturnType 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) diff --git a/test/s08-health.test.ts b/test/s08-health.test.ts index 408429c..b6e5ef3 100644 --- a/test/s08-health.test.ts +++ b/test/s08-health.test.ts @@ -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 + let mockCf: ReturnType 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) diff --git a/test/s09-no-token.test.ts b/test/s09-no-token.test.ts index 3b9fc1b..a3e5ce5 100644 --- a/test/s09-no-token.test.ts +++ b/test/s09-no-token.test.ts @@ -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) }) diff --git a/test/s11-concurrent-page-in.test.ts b/test/s11-concurrent-page-in.test.ts index c520fd3..d09493a 100644 --- a/test/s11-concurrent-page-in.test.ts +++ b/test/s11-concurrent-page-in.test.ts @@ -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 diff --git a/test/s12-page-rate-limit.test.ts b/test/s12-page-rate-limit.test.ts index 74eaf13..3a87ebc 100644 --- a/test/s12-page-rate-limit.test.ts +++ b/test/s12-page-rate-limit.test.ts @@ -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) }) diff --git a/test/s13-deploy-cooldown.test.ts b/test/s13-deploy-cooldown.test.ts index 7ec5cb9..6af8c22 100644 --- a/test/s13-deploy-cooldown.test.ts +++ b/test/s13-deploy-cooldown.test.ts @@ -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) diff --git a/test/setup.ts b/test/setup.ts index d748df6..52101b9 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -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 }) { 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; globalOutbound: null }) { + getCalls.push({ workerId }) + return { + getEntrypoint() { + return { + async fetch(request: Request): Promise { + 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 { - 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) @@ -216,4 +223,4 @@ export class MockEmbeddingService { } return this.embed(query) } -} \ No newline at end of file +}