From 3705b158bb55942dcccedcaa42cebff645fe33c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Fri, 3 Apr 2026 05:49:20 +0000 Subject: [PATCH] refactor: simplify to user-level shared capabilities - Remove agent isolation (data sovereignty belongs to user, not agent) - Capability naming: ping instead of xiaoju--ping - Route: /run/{capability} instead of /{agent}/{capability} - Auth: single deploy-token instead of per-agent tokens - Delete S10 test (agent isolation no longer exists) - Clean up old agent-prefixed workers --- src/auth.ts | 54 ++++++----------- src/backend/types.ts | 10 +--- src/backend/worker-pool.ts | 28 +++------ src/config.ts | 1 - src/kv.ts | 33 ++++++----- src/lru.ts | 8 --- src/router.ts | 45 ++++----------- test/s01-deploy.test.ts | 27 ++++----- test/s02-invoke-hit.test.ts | 17 +++--- test/s03-invoke-miss.test.ts | 32 +++++----- test/s04-eviction.test.ts | 84 +++++++++++---------------- test/s05-not-found.test.ts | 12 ++-- test/s06-remove.test.ts | 25 ++++---- test/s07-list.test.ts | 35 ++++------- test/s08-health.test.ts | 4 -- test/s09-no-token.test.ts | 5 +- test/s10-agent-isolation.test.ts | 90 ----------------------------- test/s11-concurrent-page-in.test.ts | 18 +++--- test/s12-page-rate-limit.test.ts | 29 +++++----- test/s13-deploy-cooldown.test.ts | 27 ++++----- 20 files changed, 190 insertions(+), 394 deletions(-) delete mode 100644 test/s10-agent-isolation.test.ts diff --git a/src/auth.ts b/src/auth.ts index 44a3f78..223b76d 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -26,9 +26,9 @@ export class AuthModule { /** * Validate Bearer token from Authorization header. - * Returns agent name on success, throws AuthError on failure. + * Throws AuthError on failure. */ - async validateToken(authHeader: string | null): Promise { + async validateToken(authHeader: string | null): Promise { if (!authHeader || !authHeader.startsWith('Bearer ')) { throw new AuthError(401, 'Missing or invalid Authorization header') } @@ -38,56 +38,38 @@ export class AuthModule { throw new AuthError(401, 'Empty token') } - // Scan all agents to find matching token - const agents = await this.kv.listAgents() - for (const agent of agents) { - const auth = await this.kv.getAuth(agent) - if (auth?.token === token) { - return agent - } - } - - throw new AuthError(401, 'Invalid token') - } - - /** - * Check that authenticated agent can operate on target agent's namespace. - */ - checkAgentAccess(authenticatedAgent: string, targetAgent: string): void { - if (authenticatedAgent !== targetAgent) { - throw new AuthError(403, `Agent ${authenticatedAgent} cannot access ${targetAgent}'s namespace`) + const auth = await this.kv.getDeployToken() + if (!auth || auth.token !== token) { + throw new AuthError(401, 'Invalid token') } } /** - * Check deploy cooldown for agent. Throws DeployCooldownError if active. + * Check global deploy cooldown. Throws DeployCooldownError if active. */ - async checkDeployCooldown(agent: string): Promise { - const auth = await this.kv.getAuth(agent) - if (!auth) return + async checkDeployCooldown(): Promise { + const lastDeploy = await this.kv.getLastDeployTime() + if (!lastDeploy) return const now = Date.now() - if (auth.deploy_cooldown_until && auth.deploy_cooldown_until > now) { - const retry_after = Math.ceil((auth.deploy_cooldown_until - now) / 1000) + const cooldownUntil = lastDeploy + this.config.DEPLOY_COOLDOWN_MS + if (cooldownUntil > now) { + const retry_after = Math.ceil((cooldownUntil - now) / 1000) throw new DeployCooldownError(retry_after) } } /** - * Set deploy cooldown for agent. + * Set global deploy cooldown timestamp. */ - async setDeployCooldown(agent: string): Promise { - const auth = await this.kv.getAuth(agent) - if (!auth) return - - const until = Date.now() + this.config.DEPLOY_COOLDOWN_MS - await this.kv.setAuth(agent, { ...auth, deploy_cooldown_until: until }) + async setDeployCooldown(): Promise { + await this.kv.setLastDeployTime(Date.now()) } /** - * Register a new agent with a token (used in tests). + * Set deploy token (used in tests). */ - async registerAgent(agent: string, token: string): Promise { - await this.kv.setAuth(agent, { token }) + async setToken(token: string): Promise { + await this.kv.setDeployToken({ token }) } } diff --git a/src/backend/types.ts b/src/backend/types.ts index 42585ae..e8a1d28 100644 --- a/src/backend/types.ts +++ b/src/backend/types.ts @@ -1,5 +1,4 @@ export interface DeployParams { - agent: string name: string | null // null = 自动生成 t-{hash} code: string type: 'persistent' | 'normal' | 'ephemeral' @@ -8,7 +7,7 @@ export interface DeployParams { } export interface DeployResult { - capability: string // xiaoju--ping + capability: string // 直接就是 name,如 "ping" url: string expires_at?: string cold_start: boolean @@ -16,9 +15,7 @@ export interface DeployResult { } export interface Capability { - capability: string - agent: string - name: string + capability: string // 直接就是 name,如 "ping" type: 'persistent' | 'normal' | 'ephemeral' deployed: boolean last_access: number @@ -32,7 +29,6 @@ export interface BackendStatus { backend: 'worker-pool' | 'platform' total_slots: number used_slots: number - agents: number lru_enabled: boolean eviction_count: number } @@ -41,7 +37,7 @@ export interface SigilBackend { deploy(params: DeployParams): Promise invoke(name: string, request: Request): Promise remove(name: string): Promise - list(agent?: string): Promise + list(): Promise inspect(name: string): Promise status(): Promise } diff --git a/src/backend/worker-pool.ts b/src/backend/worker-pool.ts index 2dfb826..f190059 100644 --- a/src/backend/worker-pool.ts +++ b/src/backend/worker-pool.ts @@ -36,23 +36,22 @@ export class WorkerPool implements SigilBackend { } private getWorkerName(capability: string): string { - return `${this.config.WORKER_PREFIX}${capability.replace('--', '-')}` + return `${this.config.WORKER_PREFIX}${capability}` } async deploy(params: DeployParams): Promise { - const { agent, name, code, type, ttl, bindings } = params + const { name, code, type, ttl, bindings } = params // Determine capability name - let capabilityName: string + let capability: string if (name === null) { // Generate ephemeral name: t-{6hex} const hash = await this.generateHash(code + Date.now()) - capabilityName = `t-${hash}` + capability = `t-${hash}` } else { - capabilityName = name + capability = name } - const capability = `${agent}--${capabilityName}` const workerName = this.getWorkerName(capability) const now = Date.now() @@ -91,8 +90,6 @@ export class WorkerPool implements SigilBackend { ttl, created_at: now, bindings, - agent, - name: capabilityName, }) await this.kv.setLru(capability, { last_access: now, @@ -104,7 +101,7 @@ export class WorkerPool implements SigilBackend { subdomain, }) - const url = `${this.config.GATEWAY_URL}/${agent}/${capabilityName}` + const url = `${this.config.GATEWAY_URL}/run/${capability}` const result: DeployResult = { capability, url, @@ -287,9 +284,8 @@ export class WorkerPool implements SigilBackend { await this.kv.deleteRoute(capabilityName) } - async list(agent?: string): Promise { - const prefix = agent ? `${agent}--` : undefined - const caps = await this.kv.listCapabilities(prefix) + async list(): Promise { + const caps = await this.kv.listCapabilities() const result: Capability[] = [] for (const cap of caps) { @@ -299,8 +295,6 @@ export class WorkerPool implements SigilBackend { const capability: Capability = { capability: cap, - agent: meta.agent, - name: meta.name, type: meta.type, deployed: lru.deployed, last_access: lru.last_access, @@ -326,8 +320,6 @@ export class WorkerPool implements SigilBackend { const capability: Capability = { capability: capabilityName, - agent: meta.agent, - name: meta.name, type: meta.type, deployed: lru.deployed, last_access: lru.last_access, @@ -346,13 +338,10 @@ export class WorkerPool implements SigilBackend { async status(): Promise { const caps = await this.kv.listCapabilities() let usedSlots = 0 - const agentSet = new Set() for (const cap of caps) { const lru = await this.kv.getLru(cap) - const meta = await this.kv.getMeta(cap) if (lru?.deployed) usedSlots++ - if (meta?.agent) agentSet.add(meta.agent) } const evictionCount = await this.kv.getEvictionCount() @@ -361,7 +350,6 @@ export class WorkerPool implements SigilBackend { backend: 'worker-pool', total_slots: this.config.MAX_SLOTS, used_slots: Math.min(usedSlots, this.config.MAX_SLOTS), - agents: agentSet.size, lru_enabled: true, eviction_count: evictionCount, } diff --git a/src/config.ts b/src/config.ts index 5c5ff9c..ae3fa00 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,5 @@ export const CONFIG = { MAX_SLOTS: 3, // LRU 验证用,生产 ~400 - MAX_AGENTS: 8, DEPLOY_COOLDOWN_MS: 5000, PAGE_RATE_LIMIT: 10, // 次/分钟 PAGE_RATE_WINDOW_MS: 60000, diff --git a/src/kv.ts b/src/kv.ts index f728f26..df1185e 100644 --- a/src/kv.ts +++ b/src/kv.ts @@ -9,8 +9,6 @@ export interface KvMetaValue { ttl?: number created_at: number bindings?: string[] - agent: string - name: string } export interface KvLruValue { @@ -94,13 +92,13 @@ export class KvStore { await this.kv.delete(`route:${capability}`) } - // auth:{agent} - async getAuth(agent: string): Promise { - return await this.kv.get(`auth:${agent}`, 'json') as KvAuthValue | null + // auth:deploy-token — single unified token + async getDeployToken(): Promise { + return await this.kv.get('auth:deploy-token', 'json') as KvAuthValue | null } - async setAuth(agent: string, auth: KvAuthValue): Promise { - await this.kv.put(`auth:${agent}`, JSON.stringify(auth)) + async setDeployToken(auth: KvAuthValue): Promise { + await this.kv.put('auth:deploy-token', JSON.stringify(auth)) } // stats:eviction_count @@ -126,16 +124,19 @@ export class KvStore { await this.kv.put('stats:page_rate', JSON.stringify(rate)) } - // List all capabilities by prefix scanning - async listCapabilities(prefix?: string): Promise { - const kvPrefix = prefix ? `lru:${prefix}` : 'lru:' - const list = await this.kv.list({ prefix: kvPrefix }) - return list.keys.map(k => k.name.slice('lru:'.length)) + // stats:last_deploy_time — global deploy cooldown + async getLastDeployTime(): Promise { + const v = await this.kv.get('stats:last_deploy_time', 'json') as { time: number } | null + return v?.time ?? 0 } - // List all agents (scan auth: keys) - async listAgents(): Promise { - const list = await this.kv.list({ prefix: 'auth:' }) - return list.keys.map(k => k.name.slice('auth:'.length)) + async setLastDeployTime(time: number): Promise { + await this.kv.put('stats:last_deploy_time', JSON.stringify({ time })) + } + + // List all capabilities by prefix scanning + async listCapabilities(): Promise { + const list = await this.kv.list({ prefix: 'lru:' }) + return list.keys.map(k => k.name.slice('lru:'.length)) } } diff --git a/src/lru.ts b/src/lru.ts index 5131a20..8655e7b 100644 --- a/src/lru.ts +++ b/src/lru.ts @@ -58,14 +58,6 @@ export class LruScheduler { return count } - /** - * Count distinct agents. - */ - async countAgents(): Promise { - const agents = await this.kv.listAgents() - return agents.length - } - /** * Find the best eviction candidate (lowest priority + oldest access). * Returns null if no evictable candidate found. diff --git a/src/router.ts b/src/router.ts index d265c01..bc74b43 100644 --- a/src/router.ts +++ b/src/router.ts @@ -42,12 +42,11 @@ export async function handleRequest(request: Request, env: RouterEnv): Promise { async function handleDeploy(request: Request, env: RouterEnv): Promise { try { const authHeader = request.headers.get('Authorization') - const agent = await env.auth.validateToken(authHeader) + await env.auth.validateToken(authHeader) const body = await request.json() as { - agent: string name: string | null code: string type: 'persistent' | 'normal' | 'ephemeral' @@ -72,14 +70,10 @@ async function handleDeploy(request: Request, env: RouterEnv): Promise bindings?: string[] } - // Check agent isolation - env.auth.checkAgentAccess(agent, body.agent) - // Check deploy cooldown - await env.auth.checkDeployCooldown(agent) + await env.auth.checkDeployCooldown() const result = await env.backend.deploy({ - agent: body.agent, name: body.name, code: body.code, type: body.type, @@ -88,7 +82,7 @@ async function handleDeploy(request: Request, env: RouterEnv): Promise }) // Set cooldown after successful deploy - await env.auth.setDeployCooldown(agent) + await env.auth.setDeployCooldown() return jsonOk(result, 201) } catch (e) { @@ -105,17 +99,11 @@ async function handleDeploy(request: Request, env: RouterEnv): Promise async function handleRemove(request: Request, env: RouterEnv): Promise { try { const authHeader = request.headers.get('Authorization') - const agent = await env.auth.validateToken(authHeader) + await env.auth.validateToken(authHeader) const body = await request.json() as { capability: string } const capability = body.capability - // Check agent owns this capability - const agentPrefix = `${agent}--` - if (!capability.startsWith(agentPrefix)) { - return jsonError(403, `Agent ${agent} cannot remove ${capability}`) - } - await env.backend.remove(capability) return jsonOk({ removed: capability }) } catch (e) { @@ -129,16 +117,9 @@ async function handleRemove(request: Request, env: RouterEnv): Promise async function handleList(request: Request, env: RouterEnv): Promise { try { const authHeader = request.headers.get('Authorization') - const agent = await env.auth.validateToken(authHeader) - const url = new URL(request.url) - const filterAgent = url.searchParams.get('agent') ?? undefined + await env.auth.validateToken(authHeader) - // Agent can only list their own capabilities - if (filterAgent && filterAgent !== agent) { - return jsonError(403, `Agent ${agent} cannot list ${filterAgent}'s capabilities`) - } - - const list = await env.backend.list(filterAgent ?? agent) + const list = await env.backend.list() return jsonOk({ capabilities: list }) } catch (e) { if (e instanceof AuthError) { @@ -157,12 +138,10 @@ async function handleInspect(capability: string, env: RouterEnv): Promise { - const capability = `${agent}--${capName}` try { return await env.backend.invoke(capability, request) } catch (e) { diff --git a/test/s01-deploy.test.ts b/test/s01-deploy.test.ts index 07cf336..c33685b 100644 --- a/test/s01-deploy.test.ts +++ b/test/s01-deploy.test.ts @@ -19,15 +19,14 @@ describe('S1: 部署能力', () => { kv = new KvStore(mockKv) auth = new AuthModule(kv) - // Register agent - await auth.registerAgent('xiaoju', 'token-xiaoju') + // Set unified deploy token + await auth.setToken('deploy-token') }) it('should deploy a capability via API', async () => { const req = makeRequest('POST', '/_api/deploy', { - token: 'token-xiaoju', + token: 'deploy-token', body: { - agent: 'xiaoju', name: 'ping', code: "export default { fetch() { return new Response('pong') } }", type: 'normal', @@ -42,43 +41,39 @@ describe('S1: 部署能力', () => { url: string cold_start: boolean } - expect(body.capability).toBe('xiaoju--ping') - expect(body.url).toBe('https://sigil.shazhou.workers.dev/xiaoju/ping') + expect(body.capability).toBe('ping') + expect(body.url).toBe('https://sigil.shazhou.workers.dev/run/ping') expect(body.cold_start).toBe(false) }) it('should call CfApi.deployWorker', async () => { await pool.deploy({ - agent: 'xiaoju', name: 'ping', code: "export default { fetch() { return new Response('pong') } }", type: 'normal', }) - expect(mockCf.deployCalls()).toContain('s-xiaoju-ping') + expect(mockCf.deployCalls()).toContain('s-ping') }) it('should write KV entries (code, meta, lru, route)', async () => { await pool.deploy({ - agent: 'xiaoju', name: 'ping', code: "export default { fetch() { return new Response('pong') } }", type: 'normal', }) - const code = await kv.getCode('xiaoju--ping') + const code = await kv.getCode('ping') expect(code).toBeTruthy() - const meta = await kv.getMeta('xiaoju--ping') - expect(meta?.agent).toBe('xiaoju') - expect(meta?.name).toBe('ping') + const meta = await kv.getMeta('ping') expect(meta?.type).toBe('normal') - const lru = await kv.getLru('xiaoju--ping') + const lru = await kv.getLru('ping') expect(lru?.deployed).toBe(true) expect(lru?.access_count).toBe(0) - const route = await kv.getRoute('xiaoju--ping') - expect(route?.worker_name).toBe('s-xiaoju-ping') + const route = await kv.getRoute('ping') + expect(route?.worker_name).toBe('s-ping') }) }) diff --git a/test/s02-invoke-hit.test.ts b/test/s02-invoke-hit.test.ts index 25a2bb1..b0f1a01 100644 --- a/test/s02-invoke-hit.test.ts +++ b/test/s02-invoke-hit.test.ts @@ -19,7 +19,6 @@ describe('S2: 调用已部署能力(命中)', () => { // Deploy first await pool.deploy({ - agent: 'xiaoju', name: 'ping', code: "export default { fetch() { return new Response('pong') } }", type: 'normal', @@ -28,29 +27,29 @@ describe('S2: 调用已部署能力(命中)', () => { }) it('should invoke warm capability', async () => { - const req = new Request('https://sigil.shazhou.workers.dev/xiaoju/ping') - const resp = await pool.invoke('xiaoju--ping', req) + const req = new Request('https://sigil.shazhou.workers.dev/run/ping') + const resp = await pool.invoke('ping', req) expect(resp.status).toBe(200) expect(await resp.text()).toBe('pong') }) it('should update lru.last_access on warm hit', async () => { - const lruBefore = await kv.getLru('xiaoju--ping') + const lruBefore = await kv.getLru('ping') const accessBefore = lruBefore!.last_access await new Promise(r => setTimeout(r, 5)) - const req = new Request('https://sigil.shazhou.workers.dev/xiaoju/ping') - await pool.invoke('xiaoju--ping', req) + const req = new Request('https://sigil.shazhou.workers.dev/run/ping') + await pool.invoke('ping', req) - const lruAfter = await kv.getLru('xiaoju--ping') + const lruAfter = await kv.getLru('ping') expect(lruAfter!.last_access).toBeGreaterThan(accessBefore) expect(lruAfter!.access_count).toBe(1) }) it('should NOT call deployWorker on warm hit', async () => { - const req = new Request('https://sigil.shazhou.workers.dev/xiaoju/ping') - await pool.invoke('xiaoju--ping', req) + const req = new Request('https://sigil.shazhou.workers.dev/run/ping') + await pool.invoke('ping', req) expect(mockCf.deployCalls()).toHaveLength(0) }) }) diff --git a/test/s03-invoke-miss.test.ts b/test/s03-invoke-miss.test.ts index 2d1c7c9..942da4d 100644 --- a/test/s03-invoke-miss.test.ts +++ b/test/s03-invoke-miss.test.ts @@ -18,14 +18,12 @@ describe('S3: 调用未部署能力(换入)', () => { kv = new KvStore(mockKv) // Manually write KV to simulate "evicted but not deleted from KV" state - await kv.setCode('xiaoju--ping', "export default { fetch() { return new Response('pong') } }") - await kv.setMeta('xiaoju--ping', { + await kv.setCode('ping', "export default { fetch() { return new Response('pong') } }") + await kv.setMeta('ping', { type: 'normal', created_at: Date.now() - 10000, - agent: 'xiaoju', - name: 'ping', }) - await kv.setLru('xiaoju--ping', { + await kv.setLru('ping', { last_access: Date.now() - 10000, access_count: 5, deployed: false, // key: not deployed @@ -33,33 +31,33 @@ describe('S3: 调用未部署能力(换入)', () => { }) it('should page in and call deployWorker', async () => { - const req = new Request('https://sigil.shazhou.workers.dev/xiaoju/ping') - const resp = await pool.invoke('xiaoju--ping', req) + const req = new Request('https://sigil.shazhou.workers.dev/run/ping') + const resp = await pool.invoke('ping', req) expect(resp.status).toBe(200) - expect(mockCf.deployCalls()).toContain('s-xiaoju-ping') + expect(mockCf.deployCalls()).toContain('s-ping') }) it('should set lru.deployed=true after page-in', async () => { - const req = new Request('https://sigil.shazhou.workers.dev/xiaoju/ping') - await pool.invoke('xiaoju--ping', req) + const req = new Request('https://sigil.shazhou.workers.dev/run/ping') + await pool.invoke('ping', req) - const lru = await kv.getLru('xiaoju--ping') + const lru = await kv.getLru('ping') expect(lru?.deployed).toBe(true) }) it('should set X-Sigil-Cold-Start header', async () => { - const req = new Request('https://sigil.shazhou.workers.dev/xiaoju/ping') - const resp = await pool.invoke('xiaoju--ping', req) + const req = new Request('https://sigil.shazhou.workers.dev/run/ping') + const resp = await pool.invoke('ping', req) 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/xiaoju/ping') - await pool.invoke('xiaoju--ping', req) + const req = new Request('https://sigil.shazhou.workers.dev/run/ping') + await pool.invoke('ping', req) - const route = await kv.getRoute('xiaoju--ping') - expect(route?.worker_name).toBe('s-xiaoju-ping') + const route = await kv.getRoute('ping') + expect(route?.worker_name).toBe('s-ping') }) }) diff --git a/test/s04-eviction.test.ts b/test/s04-eviction.test.ts index df3e159..8ae8a31 100644 --- a/test/s04-eviction.test.ts +++ b/test/s04-eviction.test.ts @@ -22,44 +22,40 @@ describe('S4: 配额满时换出', () => { it('should evict the coldest capability when slots are full', async () => { const baseTime = Date.now() - 100000 - // Fill up all slots (MAX_SLOTS = 10) + // Fill up all slots (MAX_SLOTS = 3) for (let i = 0; i < CONFIG.MAX_SLOTS; i++) { - const name = `cap${i}` - const capability = `xiaoju--${name}` - await kv.setCode(capability, `// code ${i}`) - await kv.setMeta(capability, { + const cap = `cap${i}` + await kv.setCode(cap, `// code ${i}`) + await kv.setMeta(cap, { type: 'normal', created_at: baseTime + i * 100, - agent: 'xiaoju', - name, }) - await kv.setLru(capability, { + await kv.setLru(cap, { last_access: baseTime + i * 100, // cap0 is coldest access_count: i, deployed: true, }) - await kv.setRoute(capability, { - worker_name: `s-xiaoju-${name}`, - subdomain: `s-xiaoju-${name}.shazhou.workers.dev`, + 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) const result = await pool.deploy({ - agent: 'xiaoju', name: 'new-cap', code: '// new', type: 'normal', }) - expect(result.capability).toBe('xiaoju--new-cap') - expect(result.evicted).toBe('xiaoju--cap0') + expect(result.capability).toBe('new-cap') + expect(result.evicted).toBe('cap0') // cap0 should have been deleted - expect(mockCf.deleteCalls()).toContain('s-xiaoju-cap0') + expect(mockCf.deleteCalls()).toContain('s-cap0') // cap0 lru should be deployed=false - const evictedLru = await kv.getLru('xiaoju--cap0') + const evictedLru = await kv.getLru('cap0') expect(evictedLru?.deployed).toBe(false) }) @@ -67,28 +63,24 @@ describe('S4: 配额满时换出', () => { const baseTime = Date.now() - 100000 for (let i = 0; i < CONFIG.MAX_SLOTS; i++) { - const name = `cap${i}` - const capability = `xiaoju--${name}` - await kv.setCode(capability, `// code ${i}`) - await kv.setMeta(capability, { + const cap = `cap${i}` + await kv.setCode(cap, `// code ${i}`) + await kv.setMeta(cap, { type: 'normal', created_at: baseTime + i * 100, - agent: 'xiaoju', - name, }) - await kv.setLru(capability, { + await kv.setLru(cap, { last_access: baseTime + i * 100, access_count: i, deployed: true, }) - await kv.setRoute(capability, { - worker_name: `s-xiaoju-${name}`, - subdomain: `s-xiaoju-${name}.shazhou.workers.dev`, + await kv.setRoute(cap, { + worker_name: `s-${cap}`, + subdomain: `s-${cap}.shazhou.workers.dev`, }) } await pool.deploy({ - agent: 'xiaoju', name: 'new-cap', code: '// new', type: 'normal', @@ -102,57 +94,51 @@ describe('S4: 配额满时换出', () => { const baseTime = Date.now() - 100000 const expiredEphemeralCreated = Date.now() - 10000 - // Fill 9 normal caps + // Fill (MAX_SLOTS - 1) normal caps for (let i = 0; i < CONFIG.MAX_SLOTS - 1; i++) { - const name = `normal${i}` - const capability = `xiaoju--${name}` - await kv.setCode(capability, `// code ${i}`) - await kv.setMeta(capability, { + const cap = `normal${i}` + await kv.setCode(cap, `// code ${i}`) + await kv.setMeta(cap, { type: 'normal', created_at: baseTime + i * 100, - agent: 'xiaoju', - name, }) - await kv.setLru(capability, { + await kv.setLru(cap, { last_access: baseTime + i * 100, access_count: 10, // high access deployed: true, }) - await kv.setRoute(capability, { - worker_name: `s-xiaoju-${name}`, - subdomain: `s-xiaoju-${name}.shazhou.workers.dev`, + await kv.setRoute(cap, { + worker_name: `s-${cap}`, + subdomain: `s-${cap}.shazhou.workers.dev`, }) } // Add 1 expired ephemeral (more recently accessed but expired) - await kv.setCode('xiaoju--ephemeral-old', '// ephemeral') - await kv.setMeta('xiaoju--ephemeral-old', { + await kv.setCode('ephemeral-old', '// ephemeral') + await kv.setMeta('ephemeral-old', { type: 'ephemeral', ttl: 1, // 1 second TTL, already expired created_at: expiredEphemeralCreated, - agent: 'xiaoju', - name: 'ephemeral-old', }) - await kv.setLru('xiaoju--ephemeral-old', { + await kv.setLru('ephemeral-old', { last_access: Date.now() - 100, // recently accessed access_count: 100, deployed: true, }) - await kv.setRoute('xiaoju--ephemeral-old', { - worker_name: 's-xiaoju-ephemeral-old', - subdomain: 's-xiaoju-ephemeral-old.shazhou.workers.dev', + 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({ - agent: 'xiaoju', name: 'newcomer', code: '// new', type: 'normal', }) // Should evict the expired ephemeral, not the coldest normal - expect(result.evicted).toBe('xiaoju--ephemeral-old') - expect(mockCf.deleteCalls()).toContain('s-xiaoju-ephemeral-old') + expect(result.evicted).toBe('ephemeral-old') + expect(mockCf.deleteCalls()).toContain('s-ephemeral-old') }) }) diff --git a/test/s05-not-found.test.ts b/test/s05-not-found.test.ts index 97a53f0..6bc679b 100644 --- a/test/s05-not-found.test.ts +++ b/test/s05-not-found.test.ts @@ -14,21 +14,21 @@ describe('S5: 调用不存在的能力', () => { }) it('should return 404 for nonexistent capability', async () => { - const req = new Request('https://sigil.shazhou.workers.dev/xiaoju/nonexistent') - const resp = await pool.invoke('xiaoju--nonexistent', req) + const req = new Request('https://sigil.shazhou.workers.dev/run/nonexistent') + const resp = await pool.invoke('nonexistent', req) expect(resp.status).toBe(404) }) it('should return error JSON body', async () => { - const req = new Request('https://sigil.shazhou.workers.dev/xiaoju/nonexistent') - const resp = await pool.invoke('xiaoju--nonexistent', req) + const req = new Request('https://sigil.shazhou.workers.dev/run/nonexistent') + const resp = await pool.invoke('nonexistent', req) const body = await resp.json() as { error: string } expect(body.error).toBeTruthy() }) it('should not call deployWorker for nonexistent', async () => { - const req = new Request('https://sigil.shazhou.workers.dev/xiaoju/nonexistent') - await pool.invoke('xiaoju--nonexistent', req) + const req = new Request('https://sigil.shazhou.workers.dev/run/nonexistent') + await pool.invoke('nonexistent', req) expect(mockCf.deployCalls()).toHaveLength(0) }) }) diff --git a/test/s06-remove.test.ts b/test/s06-remove.test.ts index 966740f..3ec7027 100644 --- a/test/s06-remove.test.ts +++ b/test/s06-remove.test.ts @@ -19,11 +19,10 @@ describe('S6: 删除能力', () => { kv = new KvStore(mockKv) auth = new AuthModule(kv) - await auth.registerAgent('xiaoju', 'token-xiaoju') + await auth.setToken('deploy-token') // Deploy first await pool.deploy({ - agent: 'xiaoju', name: 'ping', code: "export default { fetch() { return new Response('pong') } }", type: 'normal', @@ -33,32 +32,32 @@ describe('S6: 删除能力', () => { it('should call CfApi.deleteWorker', async () => { const req = makeRequest('DELETE', '/_api/remove', { - token: 'token-xiaoju', - body: { capability: 'xiaoju--ping' }, + token: 'deploy-token', + body: { capability: 'ping' }, }) const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv }) expect(resp.status).toBe(200) - expect(mockCf.deleteCalls()).toContain('s-xiaoju-ping') + expect(mockCf.deleteCalls()).toContain('s-ping') }) it('should clear all KV entries', async () => { - await pool.remove('xiaoju--ping') + await pool.remove('ping') - expect(await kv.getCode('xiaoju--ping')).toBeNull() - expect(await kv.getMeta('xiaoju--ping')).toBeNull() - expect(await kv.getLru('xiaoju--ping')).toBeNull() - expect(await kv.getRoute('xiaoju--ping')).toBeNull() + expect(await kv.getCode('ping')).toBeNull() + expect(await kv.getMeta('ping')).toBeNull() + expect(await kv.getLru('ping')).toBeNull() + expect(await kv.getRoute('ping')).toBeNull() }) it('should return removed capability in response', async () => { const req = makeRequest('DELETE', '/_api/remove', { - token: 'token-xiaoju', - body: { capability: 'xiaoju--ping' }, + token: 'deploy-token', + body: { capability: 'ping' }, }) const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv }) const body = await resp.json() as { removed: string } - expect(body.removed).toBe('xiaoju--ping') + expect(body.removed).toBe('ping') }) }) diff --git a/test/s07-list.test.ts b/test/s07-list.test.ts index 10ce707..1e5649d 100644 --- a/test/s07-list.test.ts +++ b/test/s07-list.test.ts @@ -19,50 +19,39 @@ describe('S7: 列出能力', () => { kv = new KvStore(mockKv) auth = new AuthModule(kv) - await auth.registerAgent('xiaoju', 'token-xiaoju') - await auth.registerAgent('xiaomooo', 'token-xiaomooo') + await auth.setToken('deploy-token') - // Deploy 2 for xiaoju (keep total <= MAX_SLOTS=3 to avoid eviction) - for (const name of ['ping', 'echo']) { + // Deploy some capabilities (keep <= MAX_SLOTS=3 to avoid eviction) + for (const name of ['ping', 'echo', 'hello']) { await pool.deploy({ - agent: 'xiaoju', name, code: `// ${name}`, type: 'normal', }) } - - // Deploy 1 for xiaomooo (total = 3, exactly fills slots) - await pool.deploy({ - agent: 'xiaomooo', - name: 'hello', - code: '// hello', - type: 'normal', - }) }) - it('should return only xiaoju capabilities when filtered', async () => { - const req = makeRequest('GET', '/_api/list?agent=xiaoju', { - token: 'token-xiaoju', + it('should return all capabilities', async () => { + const req = makeRequest('GET', '/_api/list', { + token: 'deploy-token', }) const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv }) expect(resp.status).toBe(200) const body = await resp.json() as { capabilities: Array<{ capability: string }> } - expect(body.capabilities).toHaveLength(2) + expect(body.capabilities).toHaveLength(3) const names = body.capabilities.map(c => c.capability) - expect(names).toContain('xiaoju--ping') - expect(names).toContain('xiaoju--echo') - expect(names).not.toContain('xiaomooo--hello') + expect(names).toContain('ping') + expect(names).toContain('echo') + expect(names).toContain('hello') }) it('should include capability metadata in response', async () => { - const caps = await pool.list('xiaoju') - expect(caps.length).toBe(2) + const caps = await pool.list() + expect(caps.length).toBe(3) for (const cap of caps) { - expect(cap.agent).toBe('xiaoju') expect(cap.type).toBe('normal') expect(cap.deployed).toBe(true) } diff --git a/test/s08-health.test.ts b/test/s08-health.test.ts index f43719c..fc413c6 100644 --- a/test/s08-health.test.ts +++ b/test/s08-health.test.ts @@ -20,9 +20,7 @@ describe('S8: 健康端点', () => { auth = new AuthModule(kv) // Deploy some capabilities - await auth.registerAgent('xiaoju', 'token-xiaoju') await pool.deploy({ - agent: 'xiaoju', name: 'ping', code: '// ping', type: 'normal', @@ -42,7 +40,6 @@ describe('S8: 健康端点', () => { backend: string total_slots: number used_slots: number - agents: number lru_enabled: boolean eviction_count: number } @@ -52,7 +49,6 @@ describe('S8: 健康端点', () => { expect(body.total_slots).toBeGreaterThan(0) expect(typeof body.used_slots).toBe('number') expect(body.used_slots).toBe(1) - expect(typeof body.agents).toBe('number') expect(body.lru_enabled).toBe(true) expect(typeof body.eviction_count).toBe('number') }) diff --git a/test/s09-no-token.test.ts b/test/s09-no-token.test.ts index 200d99c..7ef652d 100644 --- a/test/s09-no-token.test.ts +++ b/test/s09-no-token.test.ts @@ -24,7 +24,6 @@ describe('S9: 无 token 拒绝', () => { const req = makeRequest('POST', '/_api/deploy', { // No token body: { - agent: 'xiaoju', name: 'ping', code: '// ping', type: 'normal', @@ -39,7 +38,6 @@ describe('S9: 无 token 拒绝', () => { const req = makeRequest('POST', '/_api/deploy', { token: 'wrong-token', body: { - agent: 'xiaoju', name: 'ping', code: '// ping', type: 'normal', @@ -52,7 +50,7 @@ describe('S9: 无 token 拒绝', () => { it('should return 401 on DELETE without token', async () => { const req = makeRequest('DELETE', '/_api/remove', { - body: { capability: 'xiaoju--ping' }, + body: { capability: 'ping' }, }) const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv }) @@ -62,7 +60,6 @@ describe('S9: 无 token 拒绝', () => { it('should return error message in body', async () => { const req = makeRequest('POST', '/_api/deploy', { body: { - agent: 'xiaoju', name: 'ping', code: '// ping', type: 'normal', diff --git a/test/s10-agent-isolation.test.ts b/test/s10-agent-isolation.test.ts deleted file mode 100644 index a758041..0000000 --- a/test/s10-agent-isolation.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest' -import { createMockKv, createMockCfApi, makeRequest } from './setup.js' -import { WorkerPool } from '../src/backend/worker-pool.js' -import { AuthModule } from '../src/auth.js' -import { KvStore } from '../src/kv.js' -import { handleRequest } from '../src/router.js' - -describe('S10: Agent 只能操作自己的前缀', () => { - let mockKv: KVNamespace - let mockCf: ReturnType - let pool: WorkerPool - let auth: AuthModule - let kv: KvStore - - beforeEach(async () => { - mockKv = createMockKv() - mockCf = createMockCfApi() - pool = new WorkerPool(mockKv, mockCf.cfApi) - kv = new KvStore(mockKv) - auth = new AuthModule(kv) - - await auth.registerAgent('xiaoju', 'token-xiaoju') - await auth.registerAgent('xiaomooo', 'token-xiaomooo') - }) - - it('should return 403 when xiaoju tries to deploy as xiaomooo', async () => { - const req = makeRequest('POST', '/_api/deploy', { - token: 'token-xiaoju', // xiaoju's token - body: { - agent: 'xiaomooo', // but claiming xiaomooo - name: 'ping', - code: '// ping', - type: 'normal', - }, - }) - - const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv }) - expect(resp.status).toBe(403) - }) - - it('should return 403 error message', async () => { - const req = makeRequest('POST', '/_api/deploy', { - token: 'token-xiaoju', - body: { - agent: 'xiaomooo', - name: 'ping', - code: '// ping', - type: 'normal', - }, - }) - - const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv }) - const body = await resp.json() as { error: string } - expect(body.error).toContain('xiaoju') - }) - - it('should allow xiaoju to deploy their own capability', async () => { - const req = makeRequest('POST', '/_api/deploy', { - token: 'token-xiaoju', - body: { - agent: 'xiaoju', - name: 'ping', - code: '// ping', - type: 'normal', - }, - }) - - const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv }) - expect(resp.status).toBe(201) - }) - - it('should return 403 when removing another agent capability', async () => { - // First deploy xiaomooo's capability legitimately - await pool.deploy({ - agent: 'xiaomooo', - name: 'hello', - code: '// hello', - type: 'normal', - }) - - // xiaoju tries to remove it - const req = makeRequest('DELETE', '/_api/remove', { - token: 'token-xiaoju', - body: { capability: 'xiaomooo--hello' }, - }) - - const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv }) - expect(resp.status).toBe(403) - }) -}) diff --git a/test/s11-concurrent-page-in.test.ts b/test/s11-concurrent-page-in.test.ts index ad3e302..bd6699e 100644 --- a/test/s11-concurrent-page-in.test.ts +++ b/test/s11-concurrent-page-in.test.ts @@ -18,14 +18,12 @@ describe('S11: 并发换入去重', () => { kv = new KvStore(mockKv) // Simulate evicted capability: code in KV but not deployed - await kv.setCode('xiaoju--ping', "export default { fetch() { return new Response('pong') } }") - await kv.setMeta('xiaoju--ping', { + await kv.setCode('ping', "export default { fetch() { return new Response('pong') } }") + await kv.setMeta('ping', { type: 'normal', created_at: Date.now() - 10000, - agent: 'xiaoju', - name: 'ping', }) - await kv.setLru('xiaoju--ping', { + await kv.setLru('ping', { last_access: Date.now() - 10000, access_count: 0, deployed: false, @@ -33,13 +31,13 @@ describe('S11: 并发换入去重', () => { }) it('should call deployWorker only once for concurrent page-ins', async () => { - const req1 = new Request('https://sigil.shazhou.workers.dev/xiaoju/ping') - const req2 = new Request('https://sigil.shazhou.workers.dev/xiaoju/ping') + const req1 = new Request('https://sigil.shazhou.workers.dev/run/ping') + const req2 = new Request('https://sigil.shazhou.workers.dev/run/ping') // Fire concurrently const [resp1, resp2] = await Promise.all([ - pool.invoke('xiaoju--ping', req1), - pool.invoke('xiaoju--ping', req2), + pool.invoke('ping', req1), + pool.invoke('ping', req2), ]) expect(resp1.status).toBe(200) @@ -47,6 +45,6 @@ describe('S11: 并发换入去重', () => { // Should only deploy once const deployCalls = mockCf.deployCalls() - expect(deployCalls.filter(n => n === 's-xiaoju-ping')).toHaveLength(1) + expect(deployCalls.filter(n => n === 's-ping')).toHaveLength(1) }) }) diff --git a/test/s12-page-rate-limit.test.ts b/test/s12-page-rate-limit.test.ts index 22c4058..ce4b438 100644 --- a/test/s12-page-rate-limit.test.ts +++ b/test/s12-page-rate-limit.test.ts @@ -21,15 +21,12 @@ describe('S12: 换页速率限制', () => { }) async function setupCapability(name: string): Promise { - const capability = `xiaoju--${name}` - await kv.setCode(capability, `// ${name}`) - await kv.setMeta(capability, { + await kv.setCode(name, `// ${name}`) + await kv.setMeta(name, { type: 'normal', created_at: Date.now() - 10000, - agent: 'xiaoju', - name, }) - await kv.setLru(capability, { + await kv.setLru(name, { last_access: Date.now() - 10000, access_count: 0, deployed: false, // evicted @@ -43,8 +40,8 @@ describe('S12: 换页速率限制', () => { const name = `cap${i}` await setupCapability(name) - const req = new Request(`https://sigil.shazhou.workers.dev/xiaoju/${name}`) - const resp = await pool.invoke(`xiaoju--${name}`, req) + const req = new Request(`https://sigil.shazhou.workers.dev/run/${name}`) + const resp = await pool.invoke(name, req) results.push(resp.status === 200) } @@ -56,17 +53,17 @@ describe('S12: 换页速率限制', () => { for (let i = 0; i < CONFIG.PAGE_RATE_LIMIT; i++) { const name = `cap${i}` await setupCapability(name) - const req = new Request(`https://sigil.shazhou.workers.dev/xiaoju/${name}`) - await pool.invoke(`xiaoju--${name}`, req) + const req = new Request(`https://sigil.shazhou.workers.dev/run/${name}`) + await pool.invoke(name, req) } // 11th one should fail const name = `cap${CONFIG.PAGE_RATE_LIMIT}` await setupCapability(name) - const req = new Request(`https://sigil.shazhou.workers.dev/xiaoju/${name}`) + const req = new Request(`https://sigil.shazhou.workers.dev/run/${name}`) try { - const resp = await pool.invoke(`xiaoju--${name}`, req) + const resp = await pool.invoke(name, req) // If it doesn't throw, check status expect(resp.status).toBe(503) } catch (e) { @@ -79,16 +76,16 @@ describe('S12: 换页速率限制', () => { for (let i = 0; i < CONFIG.PAGE_RATE_LIMIT; i++) { const name = `cap${i}` await setupCapability(name) - const req = new Request(`https://sigil.shazhou.workers.dev/xiaoju/${name}`) - await pool.invoke(`xiaoju--${name}`, req) + const req = new Request(`https://sigil.shazhou.workers.dev/run/${name}`) + await pool.invoke(name, req) } const name = `cap${CONFIG.PAGE_RATE_LIMIT}` await setupCapability(name) - const req = new Request(`https://sigil.shazhou.workers.dev/xiaoju/${name}`) + const req = new Request(`https://sigil.shazhou.workers.dev/run/${name}`) try { - const resp = await pool.invoke(`xiaoju--${name}`, req) + const resp = await pool.invoke(name, req) if (resp.status === 503) { const body = await resp.json() as { error: string; retry_after?: number } // retry_after may be 0 for immediate window, just check it exists or we got exception diff --git a/test/s13-deploy-cooldown.test.ts b/test/s13-deploy-cooldown.test.ts index c529d0f..3b801cb 100644 --- a/test/s13-deploy-cooldown.test.ts +++ b/test/s13-deploy-cooldown.test.ts @@ -19,15 +19,14 @@ describe('S13: deploy_cooldown', () => { kv = new KvStore(mockKv) auth = new AuthModule(kv) - await auth.registerAgent('xiaoju', 'token-xiaoju') + await auth.setToken('deploy-token') }) it('should reject rapid second deploy with 429', async () => { // First deploy const req1 = makeRequest('POST', '/_api/deploy', { - token: 'token-xiaoju', + token: 'deploy-token', body: { - agent: 'xiaoju', name: 'ping', code: '// ping', type: 'normal', @@ -38,9 +37,8 @@ describe('S13: deploy_cooldown', () => { // Immediate second deploy (< 5s cooldown) const req2 = makeRequest('POST', '/_api/deploy', { - token: 'token-xiaoju', + token: 'deploy-token', body: { - agent: 'xiaoju', name: 'ping2', code: '// ping2', type: 'normal', @@ -53,15 +51,15 @@ describe('S13: deploy_cooldown', () => { it('should include retry_after in 429 response', async () => { // First deploy const req1 = makeRequest('POST', '/_api/deploy', { - token: 'token-xiaoju', - body: { agent: 'xiaoju', name: 'ping', code: '// ping', type: 'normal' }, + token: 'deploy-token', + body: { name: 'ping', code: '// ping', type: 'normal' }, }) await handleRequest(req1, { SIGIL_KV: mockKv, backend: pool, auth, kv }) // Immediate second const req2 = makeRequest('POST', '/_api/deploy', { - token: 'token-xiaoju', - body: { agent: 'xiaoju', name: 'ping2', code: '// ping2', type: 'normal' }, + token: 'deploy-token', + body: { name: 'ping2', code: '// ping2', type: 'normal' }, }) const resp2 = await handleRequest(req2, { SIGIL_KV: mockKv, backend: pool, auth, kv }) const body = await resp2.json() as { error: string; retry_after: number } @@ -71,15 +69,12 @@ describe('S13: deploy_cooldown', () => { }) it('should allow deploy after cooldown expires', async () => { - // Manually set cooldown as already expired - await kv.setAuth('xiaoju', { - token: 'token-xiaoju', - deploy_cooldown_until: Date.now() - 1000, // already past - }) + // Manually set last deploy time as already expired + await kv.setLastDeployTime(Date.now() - 10000) // 10s ago, past 5s cooldown const req = makeRequest('POST', '/_api/deploy', { - token: 'token-xiaoju', - body: { agent: 'xiaoju', name: 'ping', code: '// ping', type: 'normal' }, + token: 'deploy-token', + body: { name: 'ping', code: '// ping', type: 'normal' }, }) const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv })