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
+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)
+27 -20
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,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)
@@ -216,4 +223,4 @@ export class MockEmbeddingService {
}
return this.embed(query)
}
}
}