timers.mjs 6.23 KB
#!/usr/bin/env node
/**
 * 时间类规则的真实链路脚本,由真实 voko-worker 触发(运行前须已启动 `pnpm --filter @voko/server worker`)。
 * 用时间规则为秒级的公会(产品配置,不是测试开关)覆盖 TM-01 – TM-05;TM-04 会真实调用大模型。
 * TM-06、F0-06 在 W4。
 */
import { runScenario } from './_lib.mjs'

await runScenario('tm', async ({ db, check, call, createOwner, createGuild, createAgent, publish, waitForTask, wallet, submit, claim }) => {
  const alice = await createOwner('alice')
  const bob = await createOwner('bob')
  // fast:检查点间隔 4 秒、两类验收窗口 6 秒;slow:默认规则,只用短截止。
  const fast = await createGuild('fast', alice.user_id, { checkpoint_interval_sec: 4, piece_review_window_sec: 6, contest_review_window_sec: 6 })
  const slow = await createGuild('slow', alice.user_id)
  const fastPublisher = await createAgent('fast-publisher', alice, fast)
  const slowPublisher = await createAgent('slow-publisher', alice, slow)
  const fastTaker = await createAgent('fast-taker', bob, fast)
  const slowTaker = await createAgent('slow-taker', bob, slow)
  const eventsOf = async (agent) => (await call('GET', '/v1/events?since=0', { token: agent.key })).json.data.events

  // TM-05 open 过截止无人接 → cancelled 退款。
  const expiring = (await publish(slowPublisher, { reward: 40, deadline: new Date(Date.now() + 4000).toISOString() })).json.data
  const expired = await waitForTask(slowPublisher, expiring.id, (task) => task.status !== 'open')
  check('TM-05 open 过截止 → worker 取消并全额退款', expired.status === 'cancelled' && JSON.stringify(await wallet(slowPublisher)) === JSON.stringify({ available: 100, held: 0 }), { expired, wallet: await wallet(slowPublisher) })

  // TM-01 截止未提交 → 释放回 open、发 task.released,截止按原工期顺延。
  const late = (await publish(slowPublisher, { reward: 20, deadline: new Date(Date.now() + 6000).toISOString() })).json.data
  await claim(slowTaker, late.id)
  const reopened = await waitForTask(slowPublisher, late.id, (task) => task.status === 'open')
  const lateHold = await db('vk_task_holds').where({ task_id: late.id }).first()
  check('TM-01 截止未提交 → open、hold 记 timeout、发 task.released', reopened.status === 'open' && reopened.holder_agent_id === null && lateHold.end_reason === 'timeout' && (await eventsOf(slowTaker)).some((e) => e.type === 'task.released' && e.task_id === late.id), { reopened, lateHold })
  check('TM-01 截止顺延,任务可以重新被接', new Date(reopened.deadline) > new Date(late.deadline) && (await claim(slowTaker, late.id)).status === 200, { old: late.deadline, next: reopened.deadline })

  // TM-02 缺 1 个检查点只告警,连续缺 2 个自动释放。
  const idle = (await publish(fastPublisher, { reward: 20 })).json.data
  const idleClaim = (await claim(fastTaker, idle.id)).json.data
  const warned = await waitForTask(fastPublisher, idle.id, (task) => task.next_checkpoint_due !== idleClaim.next_checkpoint_due || task.status !== 'claimed')
  check('TM-02 缺 1 个检查点 → 仍为 claimed,到期时间顺延,并发过 task.checkpoint_due', warned.status === 'claimed' && new Date(warned.next_checkpoint_due) > new Date(idleClaim.next_checkpoint_due) && (await eventsOf(fastTaker)).some((e) => e.type === 'task.checkpoint_due' && e.task_id === idle.id), warned)
  const dropped = await waitForTask(fastPublisher, idle.id, (task) => task.status === 'open')
  const idleHold = await db('vk_task_holds').where({ task_id: idle.id }).first()
  check('TM-02 连续缺 2 个检查点 → 自动释放回 open', dropped.status === 'open' && idleHold.end_reason === 'timeout' && idleHold.has_valid_checkpoint === 0, { dropped, idleHold })

  // TM-03 计件活验收窗口到期 → 自动结算,发 task.auto_accepted。
  const silent = (await publish(fastPublisher, { reward: 30 })).json.data
  await claim(fastTaker, silent.id)
  await submit(fastTaker, silent.id)
  const auto = await waitForTask(fastPublisher, silent.id, (task) => task.status === 'settled')
  const takerEvents = await eventsOf(fastTaker)
  check('TM-03 验收窗口到期 → 自动结算,接单方 +30,双方收到 task.auto_accepted 与到期提醒', auto.status === 'settled' && (await wallet(fastTaker)).available === 130 && takerEvents.some((e) => e.type === 'task.auto_accepted' && e.task_id === silent.id) && (await eventsOf(fastPublisher)).some((e) => e.type === 'task.review_due' && e.task_id === silent.id), { auto, wallet: await wallet(fastTaker) })

  // TM-04 擂台赛截止 → reviewing;验收窗口到期 → 大模型按清单选赢家并结算;没有任何提交 → 退款。
  const finished = [{ id: 'c1', text: 'The submission summary says the work is finished.' }]
  const judged = (await publish(fastPublisher, { reward: 20, template: 'contest', acceptance: finished, deadline: new Date(Date.now() + 5000).toISOString() })).json.data
  const empty = (await publish(fastPublisher, { reward: 10, template: 'contest', acceptance: finished, deadline: new Date(Date.now() + 5000).toISOString() })).json.data
  for (const contest of [judged, empty]) await call('POST', `/v1/tasks/${contest.id}/enter`, { token: fastTaker.key })
  await submit(fastTaker, judged.id, 'The work is finished. The result is in the linked document.')
  const publisherBefore = (await wallet(fastPublisher)).available
  const takerBefore = (await wallet(fastTaker)).available
  const decided = await waitForTask(fastPublisher, judged.id, (task) => task.status === 'settled' || task.status === 'refunded', 90)
  const autoReview = await db('vk_reviews').where({ task_id: judged.id }).first()
  check('TM-04 擂台赛到期 → 大模型选赢家并结算,记一条 auto 验收', decided.status === 'settled' && (await wallet(fastTaker)).available === takerBefore + 20 && autoReview !== undefined && autoReview.reviewer_kind === 'auto', { decided, autoReview })
  const unanswered = await waitForTask(fastPublisher, empty.id, (task) => task.status === 'settled' || task.status === 'refunded', 60)
  check('TM-04 擂台赛无人提交 → 到期退款', unanswered.status === 'refunded' && (await wallet(fastPublisher)).available === publisherBefore + 10, { unanswered, wallet: await wallet(fastPublisher) })
})