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
This commit is contained in:
+18
-36
@@ -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<string> {
|
||||
async validateToken(authHeader: string | null): Promise<void> {
|
||||
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<void> {
|
||||
const auth = await this.kv.getAuth(agent)
|
||||
if (!auth) return
|
||||
async checkDeployCooldown(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await this.kv.setAuth(agent, { token })
|
||||
async setToken(token: string): Promise<void> {
|
||||
await this.kv.setDeployToken({ token })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DeployResult>
|
||||
invoke(name: string, request: Request): Promise<Response>
|
||||
remove(name: string): Promise<void>
|
||||
list(agent?: string): Promise<Capability[]>
|
||||
list(): Promise<Capability[]>
|
||||
inspect(name: string): Promise<Capability | null>
|
||||
status(): Promise<BackendStatus>
|
||||
}
|
||||
|
||||
@@ -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<DeployResult> {
|
||||
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<Capability[]> {
|
||||
const prefix = agent ? `${agent}--` : undefined
|
||||
const caps = await this.kv.listCapabilities(prefix)
|
||||
async list(): Promise<Capability[]> {
|
||||
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<BackendStatus> {
|
||||
const caps = await this.kv.listCapabilities()
|
||||
let usedSlots = 0
|
||||
const agentSet = new Set<string>()
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<KvAuthValue | null> {
|
||||
return await this.kv.get(`auth:${agent}`, 'json') as KvAuthValue | null
|
||||
// auth:deploy-token — single unified token
|
||||
async getDeployToken(): Promise<KvAuthValue | null> {
|
||||
return await this.kv.get('auth:deploy-token', 'json') as KvAuthValue | null
|
||||
}
|
||||
|
||||
async setAuth(agent: string, auth: KvAuthValue): Promise<void> {
|
||||
await this.kv.put(`auth:${agent}`, JSON.stringify(auth))
|
||||
async setDeployToken(auth: KvAuthValue): Promise<void> {
|
||||
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<string[]> {
|
||||
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<number> {
|
||||
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<string[]> {
|
||||
const list = await this.kv.list({ prefix: 'auth:' })
|
||||
return list.keys.map(k => k.name.slice('auth:'.length))
|
||||
async setLastDeployTime(time: number): Promise<void> {
|
||||
await this.kv.put('stats:last_deploy_time', JSON.stringify({ time }))
|
||||
}
|
||||
|
||||
// List all capabilities by prefix scanning
|
||||
async listCapabilities(): Promise<string[]> {
|
||||
const list = await this.kv.list({ prefix: 'lru:' })
|
||||
return list.keys.map(k => k.name.slice('lru:'.length))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,14 +58,6 @@ export class LruScheduler {
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Count distinct agents.
|
||||
*/
|
||||
async countAgents(): Promise<number> {
|
||||
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.
|
||||
|
||||
+12
-33
@@ -42,12 +42,11 @@ export async function handleRequest(request: Request, env: RouterEnv): Promise<R
|
||||
return handleInspect(capability, env)
|
||||
}
|
||||
|
||||
// GET /{agent}/{capability} — invoke
|
||||
const invokeMatch = path.match(/^\/([^/]+)\/([^/]+)$/)
|
||||
if (invokeMatch) {
|
||||
const agent = invokeMatch[1]!
|
||||
const cap = invokeMatch[2]!
|
||||
return handleInvoke(agent, cap, request, env)
|
||||
// GET /run/{capability} — invoke (no auth required)
|
||||
const runMatch = path.match(/^\/run\/([^/]+)$/)
|
||||
if (runMatch) {
|
||||
const capability = runMatch[1]!
|
||||
return handleInvoke(capability, request, env)
|
||||
}
|
||||
|
||||
return jsonError(404, 'Not found')
|
||||
@@ -61,10 +60,9 @@ async function handleHealth(env: RouterEnv): Promise<Response> {
|
||||
async function handleDeploy(request: Request, env: RouterEnv): Promise<Response> {
|
||||
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<Response>
|
||||
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<Response>
|
||||
})
|
||||
|
||||
// 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<Response>
|
||||
async function handleRemove(request: Request, env: RouterEnv): Promise<Response> {
|
||||
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<Response>
|
||||
async function handleList(request: Request, env: RouterEnv): Promise<Response> {
|
||||
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<Respon
|
||||
}
|
||||
|
||||
async function handleInvoke(
|
||||
agent: string,
|
||||
capName: string,
|
||||
capability: string,
|
||||
request: Request,
|
||||
env: RouterEnv,
|
||||
): Promise<Response> {
|
||||
const capability = `${agent}--${capName}`
|
||||
try {
|
||||
return await env.backend.invoke(capability, request)
|
||||
} catch (e) {
|
||||
|
||||
+11
-16
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
+35
-49
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
+12
-13
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
+12
-23
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<typeof createMockCfApi>
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,15 +21,12 @@ describe('S12: 换页速率限制', () => {
|
||||
})
|
||||
|
||||
async function setupCapability(name: string): Promise<void> {
|
||||
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
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user