concurrency.mjs
2.27 KB
#!/usr/bin/env node
/**
* 并发与幂等的真实链路脚本(Planning 用例 CC-01、CC-02;CC-03 结算并发在 W4)。
* CC-01 用 6 个 Agent(每个主人最多 3 个)同时发 20 个认领请求,断言恰好 1 个成功。
*/
import { randomUUID } from 'node:crypto'
import { runScenario } from './_lib.mjs'
await runScenario('cc', async ({ db, check, call, createOwner, createGuild, createAgent, publish }) => {
const alice = await createOwner('alice')
const bob = await createOwner('bob')
const carol = await createOwner('carol')
const guild = await createGuild('main', alice.user_id)
const publisher = await createAgent('publisher', alice, guild)
const takers = []
for (const [owner, tag] of [[bob, 'bob'], [carol, 'carol']]) {
for (let i = 1; i <= 3; i += 1) takers.push(await createAgent(`${tag}-${i}`, owner, guild))
}
// CC-02 同一 Idempotency-Key 并发发布 10 次 → 1 个任务、1 笔扣款。
const key = randomUUID()
const replays = await Promise.all(Array.from({ length: 10 }, () => publish(publisher, { reward: 30 }, key)))
const taskIds = new Set(replays.map((r) => r.json.data && r.json.data.id))
const wallet = (await call('GET', '/v1/wallet', { token: publisher.key })).json.data
const holds = await db('billing_ledger').where({ reason: 'vk_escrow_hold', ref_id: [...taskIds][0] })
check('CC-02 发布重放 10 次 → 1 个任务、1 笔扣款', replays.every((r) => r.status === 200) && taskIds.size === 1 && holds.length === 1 && wallet.available === 70 && wallet.held === 30, { statuses: replays.map((r) => r.status), task_ids: [...taskIds], wallet })
// CC-01 20 个并发认领同一计件活 → 恰好 1 个成功。
const taskId = [...taskIds][0]
const claims = await Promise.all(Array.from({ length: 20 }, (_, i) => call('POST', `/v1/tasks/${taskId}/claim`, { token: takers[i % takers.length].key })))
const won = claims.filter((r) => r.status === 200)
const openHolds = await db('vk_task_holds').where({ task_id: taskId }).whereNull('ended_at')
check('CC-01 20 个并发认领 → 恰好 1 个成功,其余 409', won.length === 1 && claims.filter((r) => r.status === 409).length === 19 && openHolds.length === 1 && openHolds[0].agent_id === won[0].json.data.holder_agent_id, { statuses: claims.map((r) => r.status) })
})