#!/usr/bin/env node /** * 并发与幂等的真实链路脚本(Planning 用例 CC-01、CC-02、CC-03)。CC-03 需要 voko-worker 在跑。 * 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, waitForTask, wallet: walletOf, submit, claim, review }) => { 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) }) // CC-03 发布方 accept 与自动确认 job 同时发生 → 只结算一次。验收窗口 3 秒的公会,在到期时刻连发 accept。 const fast = await createGuild('fast', alice.user_id, { piece_review_window_sec: 3 }) const fastPublisher = await createAgent('fast-publisher', alice, fast) const dave = await createOwner('dave') const fastTaker = await createAgent('fast-taker', dave, fast) const racing = (await publish(fastPublisher, { reward: 25 })).json.data await claim(fastTaker, racing.id) const submitted = await submit(fastTaker, racing.id) const dueIn = new Date(submitted.json.data.review_due).getTime() - Date.now() const accepts = [] for (const offset of [-150, 0, 150, 400, 900]) accepts.push(new Promise((resolve) => setTimeout(resolve, Math.max(0, dueIn + offset))).then(() => review(fastPublisher, racing.id, 'accept', ['c1']))) const acceptResults = await Promise.all(accepts) const settled = await waitForTask(fastPublisher, racing.id, (task) => task.status === 'settled') const racePayouts = await db('vk_escrow_payouts').where({ task_id: racing.id }) const raceLedger = await db('billing_ledger').where({ ref_id: racing.id, reason: 'vk_task_payout' }) check('CC-03 accept 与自动确认并发 → 只结算一次', settled.status === 'settled' && racePayouts.length === 1 && raceLedger.length === 1 && (await walletOf(fastTaker)).available === 125 && acceptResults.every((r) => r.status === 200 || r.status === 409) && acceptResults.filter((r) => r.status === 200).length <= 1, { statuses: acceptResults.map((r) => r.status), racePayouts }) })