callback-generation-async-smoke-test.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. 'use strict';
  2. // Regression coverage for the relay callback boundary. The callback must
  3. // return before model generation completes, while the queued job still owns
  4. // the normal quality/delivery path.
  5. const assert = require('assert/strict');
  6. const fs = require('fs');
  7. const os = require('os');
  8. const path = require('path');
  9. const { AgentWorkbenchDb } = require('../mcp/src/core/agent-workbench-db');
  10. const { AgentWorkbenchService } = require('../mcp/src/core/agent-workbench-service');
  11. const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
  12. async function waitFor(check, timeoutMs = 1000) {
  13. const deadline = Date.now() + timeoutMs;
  14. while (Date.now() < deadline) {
  15. if (check()) return;
  16. await wait(10);
  17. }
  18. assert.equal(check(), true, '后台生成在限定时间内未完成');
  19. }
  20. function createFixture(agent) {
  21. const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-callback-generation-'));
  22. const db = new AgentWorkbenchDb(path.join(dir, 'workbench.db'), {
  23. defaultMode: 'autopilot',
  24. autoSendConfidence: 0.8,
  25. });
  26. const sent = [];
  27. const service = new AgentWorkbenchService({
  28. db,
  29. agent,
  30. qiwei: {
  31. isConfigured: () => true,
  32. async sendText(toId, content) {
  33. sent.push({ toId, content });
  34. return { isSendSuccess: true, msgServerId: `callback-out-${sent.length}` };
  35. },
  36. },
  37. config: {
  38. accountKey: 'callback-generation-smoke',
  39. agent: { provider: 'fixture', model: 'fixture', apiKey: 'fixture', generationRetryAttempts: 1, generationRetryBaseMs: 0, qualityPassScore: 60 },
  40. qiwei: { allowedSenders: ['callback-contact'] },
  41. memory: { enabled: false },
  42. },
  43. });
  44. return { dir, db, service, sent };
  45. }
  46. function output() {
  47. return {
  48. content: '已收到,我先按当前信息整理下一步。',
  49. confidence: 0.96,
  50. intent: '信息确认',
  51. reason: '异步回调测试固定输出。',
  52. requiresHuman: false,
  53. profileUpdates: {},
  54. tasks: [],
  55. alerts: [],
  56. citations: [],
  57. toolTrace: [],
  58. };
  59. }
  60. async function main() {
  61. let release;
  62. const generationGate = new Promise(resolve => { release = resolve; });
  63. let calls = 0;
  64. const fixture = createFixture({
  65. modelClient: { isConfigured: () => true },
  66. async run() {
  67. calls += 1;
  68. await generationGate;
  69. return output();
  70. },
  71. });
  72. try {
  73. const startedAt = Date.now();
  74. const queued = await fixture.service.ingestInbound({
  75. externalId: 'callback-async-1',
  76. contactId: 'callback-contact',
  77. contactName: '回调异步测试',
  78. content: '请介绍一下服务',
  79. }, { awaitGeneration: false, allowAutoSend: true });
  80. assert.equal(queued.status, 'generation_queued');
  81. assert(Date.now() - startedAt < 300, '回调不应等待模型生成');
  82. assert.equal(fixture.sent.length, 0, '模型完成前不能发送');
  83. await waitFor(() => calls === 1);
  84. release();
  85. await waitFor(() => fixture.sent.length === 1);
  86. assert.equal(fixture.sent[0].toId, 'callback-contact');
  87. assert.equal(fixture.db.listAudit(100).some(item => item.action === 'autopilot_message_sent'), true);
  88. } finally {
  89. fixture.service.stopBackgroundWorkers();
  90. fixture.db.close();
  91. fs.rmSync(fixture.dir, { recursive: true, force: true });
  92. }
  93. const failed = createFixture({
  94. modelClient: { isConfigured: () => true },
  95. async run() { throw new Error('fixture generation contract error'); },
  96. });
  97. try {
  98. const queued = await failed.service.ingestInbound({
  99. externalId: 'callback-async-failure',
  100. contactId: 'callback-contact',
  101. contactName: '回调失败测试',
  102. content: '普通问题',
  103. }, { awaitGeneration: false, allowAutoSend: true });
  104. assert.equal(queued.status, 'generation_queued');
  105. await waitFor(() => failed.db.listAudit(100).some(item => item.action === 'agent_failed'));
  106. assert.equal(failed.sent.length, 0, '生成失败不得发送空消息');
  107. } finally {
  108. failed.service.stopBackgroundWorkers();
  109. failed.db.close();
  110. fs.rmSync(failed.dir, { recursive: true, force: true });
  111. }
  112. process.stdout.write(JSON.stringify({ status: 'passed', checks: [
  113. 'relay callback returns generation_queued before model completion',
  114. 'background generation preserves autopilot delivery',
  115. 'background failure is audited without crashing the callback',
  116. ] }, null, 2) + '\n');
  117. }
  118. main().catch(error => {
  119. process.stderr.write(JSON.stringify({ status: 'failed', message: error.message, stack: error.stack }, null, 2) + '\n');
  120. process.exitCode = 1;
  121. });