verify.mjs 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. // Run after npm run build. Uses only a temporary SQLite home and loopback fake
  2. // Hermes; no existing service, account, model, or group is contacted.
  3. import assert from 'node:assert/strict'
  4. import { mkdtempSync, rmSync } from 'node:fs'
  5. import { tmpdir } from 'node:os'
  6. import { join } from 'node:path'
  7. import { randomUUID } from 'node:crypto'
  8. import { WebSocketServer } from 'ws'
  9. import { WorkspaceRuntime } from '../../../dist-server/server/workspaceRuntime.js'
  10. import { WorkspaceStore } from '../../../dist-server/server/workspaceStore.js'
  11. import { UploadStore } from '../../../dist-server/server/uploads.js'
  12. const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
  13. const receipt = '收到 @审核 终审维持通过。\n交付状态(不变):入草稿箱 Media ID `fixture-draft`,发布目录 `/fixture/final/`。\n等用户群发指令。'
  14. async function verify(mode, closingReceipt, expectedCalls) {
  15. const home = mkdtempSync(join(tmpdir(), 'yaoyao-relay-acceptance-'))
  16. const store = new WorkspaceStore(home), uploads = new UploadStore(home)
  17. const server = new WebSocketServer({ host: '127.0.0.1', port: 0 })
  18. let runtime, calls = 0
  19. try {
  20. await new Promise(resolve => server.once('listening', resolve))
  21. const target = {
  22. url: new URL(`http://127.0.0.1:${server.address().port}`), client: {},
  23. session: {
  24. webSocketCredential: async () => ({ name: 'ticket', value: 'fixture' }),
  25. request: async () => ({ status: 200, body: Buffer.from('{}') }),
  26. },
  27. }
  28. server.on('connection', socket => {
  29. socket.send(JSON.stringify({ method: 'event', params: { type: 'gateway.ready' } }))
  30. socket.on('message', raw => {
  31. const frame = JSON.parse(String(raw)), respond = result => socket.send(JSON.stringify({ id: frame.id, result }))
  32. if (frame.method === 'session.create' || frame.method === 'session.resume') {
  33. respond({ session_id: randomUUID(), stored_session_id: frame.params.session_id ?? randomUUID(), running: false, info: { profile_name: 'default' } })
  34. } else if (frame.method === 'prompt.submit') {
  35. calls++; respond({ status: 'streaming' })
  36. const lead = frame.params.text.startsWith('你是 竹儿。')
  37. const text = calls > 20 ? '完成' : lead ? closingReceipt && calls > 1 ? receipt : '@审核 请再次检查文案'
  38. : mode === 'host' ? '审核结果相同' : '@竹儿 请再次检查文案'
  39. socket.send(JSON.stringify({ method: 'event', params: { type: 'message.complete', session_id: frame.params.session_id, payload: { text, status: 'complete' } } }))
  40. } else respond({ ok: true })
  41. })
  42. })
  43. runtime = new WorkspaceRuntime(store, { requireSource() {}, target: () => target }, uploads)
  44. const owner = 'isolated-fixture'
  45. const a = store.createAgent(owner, { name: '竹儿', profile: 'default', instructions: '' })
  46. const b = store.createAgent(owner, { name: '审核', profile: 'default', instructions: '' })
  47. const group = store.createGroup(owner, { name: '隔离验收', memberIds: [a.id, b.id], administratorId: a.id, mode, maxReplyRounds: -1 })
  48. const run = runtime.send(owner, group.id, { requestId: randomUUID(), content: '@竹儿 开始' })
  49. const deadline = Date.now() + 10_000
  50. while (store.require(owner, 'run', run.id).status !== 'complete' && Date.now() < deadline) await sleep(10)
  51. assert.equal(store.require(owner, 'run', run.id).status, 'complete')
  52. assert.equal(calls, expectedCalls)
  53. await sleep(250)
  54. assert.equal(calls, expectedCalls, 'no further calls after settling')
  55. const pending = store.list(owner, 'turn').filter(w => !w.planned || !['complete', 'failed', 'interrupted'].includes(w.status))
  56. assert.equal(pending.length, 0)
  57. assert.equal(store.require(owner, 'conversation', group.id).activeRunId, undefined)
  58. const notices = store.messages(owner, group.id).filter(m => m.role === 'system' && m.content.includes('检测到重复协作'))
  59. assert.equal(notices.length, closingReceipt ? 0 : 1)
  60. return { mode, closingReceipt, calls, status: 'complete', pending: pending.length, notices: notices.length, stableAfterMs: 250 }
  61. } finally {
  62. runtime?.close()
  63. for (const socket of server.clients) socket.terminate()
  64. await new Promise(resolve => server.close(resolve))
  65. await sleep(10)
  66. uploads.close(); store.close()
  67. rmSync(home, { recursive: true, force: true })
  68. }
  69. }
  70. const results = []
  71. for (const [mode, closingReceipt, calls] of [['host', true, 3], ['host', false, 5], ['free', false, 4]]) {
  72. results.push(await verify(mode, closingReceipt, calls))
  73. }
  74. console.log(JSON.stringify({ source: 'dist-server', network: 'loopback fixture only', results }, null, 2))