agent-runtime-reliability-smoke-test.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. 'use strict';
  2. const assert = require('assert/strict');
  3. const fs = require('fs');
  4. const os = require('os');
  5. const path = require('path');
  6. const { AgentWorkbenchDb } = require('../mcp/src/core/agent-workbench-db');
  7. const { AgentWorkbenchService } = require('../mcp/src/core/agent-workbench-service');
  8. const { QiweiAgentPoller } = require('../mcp/src/dashboard/agent-service').__testing;
  9. const { GroupAgentService } = require('../mcp/src/dashboard/group-agent-service');
  10. const { extractExplicitCustomerIntelligence } = require('../mcp/src/core/agent-runtime');
  11. const results = [];
  12. function fixtureDir(prefix) {
  13. return fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
  14. }
  15. function closeFixture({ service, db, dir }) {
  16. service?.stopBackgroundWorkers?.();
  17. db?.close?.();
  18. if (dir) fs.rmSync(dir, { recursive: true, force: true });
  19. }
  20. function qualifiedOutput(content = '已收到,我先按当前信息整理下一步。') {
  21. return {
  22. content,
  23. confidence: 0.96,
  24. intent: '信息确认',
  25. reason: '可靠性测试固定输出。',
  26. requiresHuman: false,
  27. profileUpdates: {},
  28. tasks: [],
  29. alerts: [],
  30. citations: [],
  31. toolTrace: [],
  32. };
  33. }
  34. async function check(name, fn) {
  35. await fn();
  36. results.push({ name, status: 'passed' });
  37. }
  38. async function testBatchIsolationAndCursorCommit() {
  39. const dir = fixtureDir('qiwei-poller-isolation');
  40. const db = new AgentWorkbenchDb(path.join(dir, 'workbench.db'), { defaultMode: 'review' });
  41. const processed = [];
  42. let syncCalls = 0;
  43. const poller = new QiweiAgentPoller({
  44. config: { messageRetryAttempts: 1, messageRetryBaseMs: 0, intervalMs: 0 },
  45. db,
  46. qiwei: {
  47. async syncMessages() {
  48. syncCalls += 1;
  49. return syncCalls === 1
  50. ? { syncMsgList: [{ seq: 1 }, { seq: 2 }, { seq: 3 }], travelSyncKey: 3 }
  51. : { syncMsgList: [], travelSyncKey: 3 };
  52. },
  53. },
  54. service: {},
  55. });
  56. poller.process = async message => {
  57. processed.push(Number(message.seq));
  58. if (Number(message.seq) === 2) throw new Error('fixture message failure');
  59. return { status: 'processed' };
  60. };
  61. poller.running = true;
  62. poller.waitInterval = async () => { poller.running = false; };
  63. try {
  64. await poller.loop(0);
  65. assert.deepEqual(processed, [1, 2, 3], '同一批次后续消息必须继续处理');
  66. assert.equal(Number(db.getPollState('sync_key', '0')), 3, '批次完成后游标必须推进');
  67. assert.equal(poller.metrics.failed, 1);
  68. assert.equal(poller.metrics.lastBatchFailed, 1);
  69. assert.equal(db.listAudit(100).some(item => item.action === 'poller_message_error'), true);
  70. } finally {
  71. closeFixture({ db, dir });
  72. }
  73. }
  74. async function testStalledCursorDoesNotSpin() {
  75. const dir = fixtureDir('qiwei-poller-stall');
  76. const db = new AgentWorkbenchDb(path.join(dir, 'workbench.db'), { defaultMode: 'review' });
  77. let syncCalls = 0;
  78. let waits = 0;
  79. const poller = new QiweiAgentPoller({
  80. config: { messageRetryAttempts: 1, messageRetryBaseMs: 0, intervalMs: 0 },
  81. db,
  82. qiwei: {
  83. async syncMessages() {
  84. syncCalls += 1;
  85. return { syncMsgList: [{ seq: 4 }], travelSyncKey: 4 };
  86. },
  87. },
  88. service: {},
  89. });
  90. poller.process = async () => ({ status: 'ignored_not_allowlisted' });
  91. poller.running = true;
  92. poller.waitInterval = async () => {
  93. waits += 1;
  94. poller.running = false;
  95. };
  96. try {
  97. await poller.loop(4);
  98. assert.equal(syncCalls, 1, '停滞游标必须经过等待,不得在同一轮忙循环');
  99. assert.equal(waits, 1);
  100. assert.equal(poller.metrics.cursorStalls, 1);
  101. assert.equal(db.listAudit(100).some(item => item.action === 'poller_cursor_stalled'), true);
  102. assert.equal(Number(db.getPollState('sync_key', '0')), 4, '停滞批次不得伪造游标');
  103. } finally {
  104. closeFixture({ db, dir });
  105. }
  106. }
  107. async function testPollerGenerationRetry() {
  108. const dir = fixtureDir('qiwei-poller-retry');
  109. const db = new AgentWorkbenchDb(path.join(dir, 'workbench.db'), { defaultMode: 'review' });
  110. let processCalls = 0;
  111. let draftCalls = 0;
  112. const poller = new QiweiAgentPoller({
  113. config: { messageRetryAttempts: 2, messageRetryBaseMs: 0, intervalMs: 0 },
  114. db,
  115. qiwei: {},
  116. service: {
  117. async generateLatestDraft() {
  118. draftCalls += 1;
  119. return { status: 'pending_review' };
  120. },
  121. },
  122. });
  123. poller.process = async () => {
  124. processCalls += 1;
  125. return {
  126. status: 'agent_failed',
  127. errorCode: 'network_error',
  128. error: 'network timeout',
  129. conversation: { id: 'conversation-retry' },
  130. message: { id: 'message-retry' },
  131. };
  132. };
  133. try {
  134. const result = await poller.processWithRetry({ seq: 11, msgServerId: 'external-retry' });
  135. assert.equal(result.status, 'pending_review');
  136. assert.equal(processCalls, 1, '已入库消息重试不得重新回放 wire message');
  137. assert.equal(draftCalls, 1);
  138. assert.equal(db.listAudit(100).some(item => item.action === 'poller_message_retry_scheduled'), true);
  139. } finally {
  140. closeFixture({ db, dir });
  141. }
  142. }
  143. async function testPollerAutopilotSendRetry() {
  144. const dir = fixtureDir('qiwei-poller-send-retry');
  145. const db = new AgentWorkbenchDb(path.join(dir, 'workbench.db'), { defaultMode: 'autopilot' });
  146. let processCalls = 0;
  147. let draftCalls = 0;
  148. const poller = new QiweiAgentPoller({
  149. config: { messageRetryAttempts: 2, messageRetryBaseMs: 0, intervalMs: 0 },
  150. db,
  151. qiwei: {},
  152. service: {
  153. async generateLatestDraft(conversationId) {
  154. draftCalls += 1;
  155. return {
  156. status: 'autopilot_sent',
  157. conversation: { id: conversationId },
  158. message: { id: 'message-send-retry' },
  159. reply: { id: 'outbound-send-retry' },
  160. };
  161. },
  162. },
  163. });
  164. poller.process = async () => {
  165. processCalls += 1;
  166. return {
  167. status: 'autopilot_send_failed',
  168. errorCode: 'upstream_send_not_acknowledged',
  169. error: '企微上游暂时未确认发送结果',
  170. conversation: { id: 'conversation-send-retry' },
  171. message: { id: 'message-send-retry' },
  172. };
  173. };
  174. try {
  175. const result = await poller.processWithRetry({ seq: 12, msgServerId: 'external-send-retry' });
  176. assert.equal(result.status, 'autopilot_sent');
  177. assert.equal(processCalls, 1, '发送失败重试不得重新回放原始企微消息');
  178. assert.equal(draftCalls, 1, '发送失败应只重新生成最新草稿一次');
  179. assert.equal(db.listAudit(100).some(item => item.action === 'poller_message_retry_scheduled'), true);
  180. } finally {
  181. closeFixture({ db, dir });
  182. }
  183. }
  184. async function waitFor(predicate, message) {
  185. for (let attempt = 0; attempt < 80; attempt += 1) {
  186. if (predicate()) return;
  187. await new Promise(resolve => setTimeout(resolve, 5));
  188. }
  189. throw new Error(message);
  190. }
  191. function groupFixture(dir, options = {}) {
  192. const messages = [];
  193. const roomId = 'reliability-group';
  194. const groups = { [roomId]: { roomName: '可靠性测试群' } };
  195. const statePath = path.join(dir, 'group-agent-replies.json');
  196. const runtime = {
  197. db: { globalState: () => ({ paused: false }) },
  198. config: { qiwei: { groupSendRetryAttempts: options.sendAttempts ?? 2, groupSendRetryBaseMs: 0 } },
  199. agent: options.agent,
  200. qiwei: options.qiwei,
  201. };
  202. const service = new GroupAgentService({
  203. projectRoot: dir,
  204. statePath,
  205. getRuntime: () => runtime,
  206. loadGroups: () => groups,
  207. loadMessages: () => messages,
  208. appendMessage: (_id, message) => messages.push(message),
  209. });
  210. const inbound = (id, content) => ({
  211. fromRoomId: roomId,
  212. msgUniqueIdentifier: id,
  213. msgType: 1,
  214. seq: messages.length + 1,
  215. senderId: 'group-customer',
  216. senderName: '群客户',
  217. content,
  218. timestamp: Date.now(),
  219. });
  220. return { service, roomId, inbound };
  221. }
  222. async function testGroupDeferredGenerationQueuesEveryInbound() {
  223. const dir = fixtureDir('qiwei-group-deferred');
  224. let agentCalls = 0;
  225. let releaseFirst;
  226. const firstGate = new Promise(resolve => { releaseFirst = resolve; });
  227. const { service, inbound } = groupFixture(dir, {
  228. agent: {
  229. async run() {
  230. agentCalls += 1;
  231. if (agentCalls === 1) await firstGate;
  232. return qualifiedOutput(`群聊草稿 ${agentCalls}`);
  233. },
  234. },
  235. qiwei: { async sendText() { return { isSendSuccess: true }; } },
  236. });
  237. try {
  238. const first = await service.ingestPolledMessage(inbound('group-deferred-1', '第一条群消息'), {}, { deferGeneration: true });
  239. const second = await service.ingestPolledMessage(inbound('group-deferred-2', '第二条群消息'), {}, { deferGeneration: true });
  240. assert.equal(first.status, 'group_generation_queued');
  241. assert.equal(second.status, 'group_generation_queued');
  242. await waitFor(() => agentCalls === 1, '首条群消息必须开始后台生成');
  243. releaseFirst();
  244. await waitFor(() => agentCalls === 2, '生成期间到达的第二条群消息必须顺序进入生成队列');
  245. await waitFor(() => Boolean(service.list()[0].pendingReply), '群聊生成完成后必须留下待审核草稿');
  246. const pending = service.list()[0].pendingReply;
  247. assert.equal(pending.sourceMessageId, 'group-deferred-2', '最新已处理群消息必须获得独立草稿');
  248. } finally {
  249. fs.rmSync(dir, { recursive: true, force: true });
  250. }
  251. }
  252. async function testGroupAutoSendRetriesWithoutFalseSent() {
  253. const dir = fixtureDir('qiwei-group-send-retry');
  254. let sendCalls = 0;
  255. const { service, roomId, inbound } = groupFixture(dir, {
  256. sendAttempts: 2,
  257. agent: { async run() { return qualifiedOutput('群聊自动回复'); } },
  258. qiwei: {
  259. async sendText() {
  260. sendCalls += 1;
  261. return sendCalls === 1 ? { isSendSuccess: false } : { isSendSuccess: true };
  262. },
  263. },
  264. });
  265. try {
  266. service.setMode(roomId, 'auto');
  267. const result = await service.ingestPolledMessage(inbound('group-send-retry-1', '请介绍一下服务'));
  268. assert.equal(result.status, 'auto_sent');
  269. assert.equal(sendCalls, 2, '上游首次未确认时必须有限重试');
  270. const group = service.list()[0];
  271. assert.equal(group.pendingReply, null, '确认发送后不得保留 pending 草稿');
  272. assert.equal(group.audit.some(item => item.action === 'group_send_retry_scheduled'), true);
  273. assert.equal(group.audit.some(item => item.action === 'group_message_auto_sent'), true);
  274. } finally {
  275. fs.rmSync(dir, { recursive: true, force: true });
  276. }
  277. }
  278. async function testGroupAutoSendFailureKeepsPendingDraft() {
  279. const dir = fixtureDir('qiwei-group-send-failed');
  280. let sendCalls = 0;
  281. const { service, roomId, inbound } = groupFixture(dir, {
  282. sendAttempts: 1,
  283. agent: { async run() { return qualifiedOutput('仍需人工确认的群聊回复'); } },
  284. qiwei: { async sendText() { sendCalls += 1; return { isSendSuccess: false }; } },
  285. });
  286. try {
  287. service.setMode(roomId, 'auto');
  288. const result = await service.ingestPolledMessage(inbound('group-send-failed-1', '我需要帮助'));
  289. assert.equal(result.status, 'pending_review');
  290. assert.equal(sendCalls, 1);
  291. const group = service.list()[0];
  292. assert.equal(group.pendingReply?.status, 'pending', '未确认投递的群消息不能伪造为 sent');
  293. assert.equal(group.audit.some(item => item.action === 'group_auto_send_failed'), true);
  294. assert.equal(group.audit.some(item => item.action === 'group_message_auto_sent'), false);
  295. } finally {
  296. fs.rmSync(dir, { recursive: true, force: true });
  297. }
  298. }
  299. async function testGenerationRetryAndRealtimeIntelligence() {
  300. const dir = fixtureDir('qiwei-runtime-reliability');
  301. const db = new AgentWorkbenchDb(path.join(dir, 'workbench.db'), {
  302. defaultMode: 'review',
  303. autoSendConfidence: 0.88,
  304. });
  305. let agentCalls = 0;
  306. let releaseAgent;
  307. const gate = new Promise(resolve => { releaseAgent = resolve; });
  308. const service = new AgentWorkbenchService({
  309. db,
  310. agent: {
  311. modelClient: { isConfigured: () => true },
  312. async run() {
  313. agentCalls += 1;
  314. if (agentCalls === 1) return { content: '' };
  315. await gate;
  316. return qualifiedOutput();
  317. },
  318. },
  319. qiwei: {
  320. isConfigured: () => true,
  321. async sendText() { return { isSendSuccess: true }; },
  322. },
  323. config: {
  324. accountKey: 'reliability-fixture',
  325. agent: {
  326. provider: 'fixture',
  327. model: 'fixture',
  328. apiKey: 'fixture',
  329. generationRetryAttempts: 2,
  330. generationRetryBaseMs: 0,
  331. qualityPassScore: 60,
  332. },
  333. qiwei: { allowedSenders: ['contact-realtime'] },
  334. memory: { enabled: false, extractionIntervalMs: 60000 },
  335. },
  336. });
  337. try {
  338. const inbound = service.ingestInbound({
  339. externalId: 'realtime-intelligence-1',
  340. contactId: 'contact-realtime',
  341. contactName: '实时画像测试',
  342. content: '预算 300 万,明年置换',
  343. });
  344. // The model is deliberately held. Rule-based intelligence must already
  345. // be queryable while generation is waiting.
  346. await new Promise(resolve => setImmediate(resolve));
  347. const conversation = db.getConversationByContactId('contact-realtime');
  348. const profile = db.getProfile(conversation.id).profile;
  349. assert.equal(profile.budgetWan, 300);
  350. assert.equal(profile.purpose, '置换');
  351. assert.equal(profile.timeline, '明年');
  352. assert.equal(db.listCustomerAlerts(conversation.id).some(item => item.business_key === 'high_intent:core_demand_ready'), true);
  353. assert.equal(db.listAudit(100, conversation.id).some(item => item.action === 'customer_intelligence_realtime_updated'), true);
  354. releaseAgent();
  355. const result = await inbound;
  356. assert.equal(result.status, 'pending_review');
  357. assert.equal(agentCalls, 2, '空回复应触发一次模型重试');
  358. assert.equal(db.listAudit(100, conversation.id).some(item => item.action === 'agent_generation_retry_scheduled'), true);
  359. } finally {
  360. closeFixture({ service, db, dir });
  361. }
  362. }
  363. async function testGenerationFallbackAutoReply() {
  364. const dir = fixtureDir('qiwei-generation-fallback');
  365. const db = new AgentWorkbenchDb(path.join(dir, 'workbench.db'), {
  366. defaultMode: 'autopilot',
  367. autoSendConfidence: 0.88,
  368. });
  369. let agentCalls = 0;
  370. let sendCalls = 0;
  371. const service = new AgentWorkbenchService({
  372. db,
  373. agent: {
  374. modelClient: { isConfigured: () => true },
  375. async run() {
  376. agentCalls += 1;
  377. throw new Error('Claude Code 处理超时');
  378. },
  379. },
  380. qiwei: {
  381. isConfigured: () => true,
  382. async sendText() {
  383. sendCalls += 1;
  384. return { isSendSuccess: true };
  385. },
  386. },
  387. config: {
  388. accountKey: 'fallback-fixture',
  389. agent: {
  390. provider: 'fixture',
  391. model: 'fixture',
  392. apiKey: 'fixture',
  393. generationRetryAttempts: 1,
  394. generationRetryBaseMs: 0,
  395. qualityPassScore: 60,
  396. },
  397. qiwei: { allowedSenders: ['contact-fallback'] },
  398. memory: { enabled: false, extractionIntervalMs: 60000 },
  399. },
  400. });
  401. try {
  402. const greeting = await service.ingestInbound({
  403. externalId: 'fallback-greeting-1',
  404. contactId: 'contact-fallback',
  405. contactName: '自动回复兜底测试',
  406. content: '你好',
  407. });
  408. assert.equal(greeting.status, 'autopilot_sent');
  409. assert.equal(agentCalls, 0, '简单问候应绕过易超时的模型调用');
  410. assert.equal(sendCalls, 1, '简单问候必须实际发送一次');
  411. assert.equal(db.listAudit(100).some(item => item.action === 'agent_generation_shortcut'), true);
  412. assert.equal(db.listAudit(100).some(item => item.action === 'autopilot_message_sent'), true);
  413. const general = await service.ingestInbound({
  414. externalId: 'fallback-general-1',
  415. contactId: 'contact-fallback',
  416. contactName: '自动回复兜底测试',
  417. content: '我想了解一下服务',
  418. });
  419. assert.equal(general.status, 'autopilot_sent');
  420. assert.equal(agentCalls, 1, '普通消息仍先尝试模型');
  421. assert.equal(sendCalls, 2, '模型超时后应发送即时确认');
  422. assert.equal(db.listAudit(100).some(item => item.action === 'agent_generation_fallback_created'), true);
  423. } finally {
  424. closeFixture({ service, db, dir });
  425. }
  426. }
  427. async function testRestartRecoversAcknowledgedGeneration() {
  428. const dir = fixtureDir('qiwei-generation-recovery');
  429. const db = new AgentWorkbenchDb(path.join(dir, 'workbench.db'), {
  430. defaultMode: 'autopilot',
  431. autoSendConfidence: 0.88,
  432. });
  433. const config = {
  434. accountKey: 'generation-recovery-fixture',
  435. agent: {
  436. provider: 'fixture',
  437. model: 'fixture',
  438. apiKey: 'fixture',
  439. generationRetryAttempts: 1,
  440. generationRetryBaseMs: 0,
  441. generationRecovery: { pendingAgeMs: 5000, retryCooldownMs: 5000, maxAttempts: 2 },
  442. qualityPassScore: 60,
  443. },
  444. qiwei: { allowedSenders: ['contact-recovery'] },
  445. memory: { enabled: false },
  446. };
  447. const initial = new AgentWorkbenchService({
  448. db,
  449. agent: { modelClient: { isConfigured: () => true }, async run() { return new Promise(() => {}); } },
  450. qiwei: { isConfigured: () => true, async sendText() { return { isSendSuccess: true }; } },
  451. config,
  452. });
  453. let recovered;
  454. try {
  455. const queued = await initial.ingestInbound({
  456. externalId: 'generation-recovery-1',
  457. contactId: 'contact-recovery',
  458. contactName: '恢复测试客户',
  459. content: '我想了解一下服务',
  460. }, { awaitGeneration: false });
  461. assert.equal(queued.status, 'generation_queued');
  462. const conversation = db.getConversationByContactId('contact-recovery');
  463. assert.equal(db.getMessage(queued.message.id).status, 'generation_pending');
  464. db.db.prepare("UPDATE messages SET created_at=? WHERE id=?").run(new Date(Date.now() - 6000).toISOString(), queued.message.id);
  465. let sendCalls = 0;
  466. recovered = new AgentWorkbenchService({
  467. db,
  468. agent: { modelClient: { isConfigured: () => true }, async run() { throw new Error('network timeout'); } },
  469. qiwei: {
  470. isConfigured: () => true,
  471. async sendText() { sendCalls += 1; return { isSendSuccess: true }; },
  472. },
  473. config,
  474. });
  475. const result = await recovered.recoverPendingGenerations('fixture:restart');
  476. assert.equal(result.recovered, 1, '已 ACK 但未生成的入站消息必须在重启后恢复');
  477. assert.equal(sendCalls, 1, `恢复任务在全自动模式下必须完成实际投递:${JSON.stringify(result)}`);
  478. assert.equal(db.getMessage(queued.message.id).status, 'reply_sent');
  479. assert.equal(db.listAudit(100, conversation.id).some(item => item.action === 'agent_generation_recovery_completed'), true);
  480. } finally {
  481. initial.stopBackgroundWorkers();
  482. recovered?.stopBackgroundWorkers();
  483. db.close();
  484. fs.rmSync(dir, { recursive: true, force: true });
  485. }
  486. }
  487. async function testRiskFallbackStaysHumanReview() {
  488. const dir = fixtureDir('qiwei-generation-fallback-risk');
  489. const db = new AgentWorkbenchDb(path.join(dir, 'workbench.db'), { defaultMode: 'autopilot' });
  490. let sendCalls = 0;
  491. const service = new AgentWorkbenchService({
  492. db,
  493. agent: {
  494. modelClient: { isConfigured: () => true },
  495. async run() { throw new Error('network timeout'); },
  496. },
  497. qiwei: {
  498. isConfigured: () => true,
  499. async sendText() { sendCalls += 1; return { isSendSuccess: true }; },
  500. },
  501. config: {
  502. accountKey: 'fallback-risk-fixture',
  503. agent: { provider: 'fixture', model: 'fixture', apiKey: 'fixture', generationRetryAttempts: 1, generationRetryBaseMs: 0, qualityPassScore: 60 },
  504. qiwei: { allowedSenders: ['contact-risk'] },
  505. memory: { enabled: false, extractionIntervalMs: 60000 },
  506. },
  507. });
  508. try {
  509. const result = await service.ingestInbound({
  510. externalId: 'fallback-risk-1',
  511. contactId: 'contact-risk',
  512. contactName: '风险兜底测试',
  513. content: '我要投诉这个问题',
  514. });
  515. assert.equal(result.status, 'pending_review');
  516. assert.equal(sendCalls, 0, '风险消息的兜底只能进入人工审核');
  517. assert.equal(Boolean(result.draft.requires_human), true);
  518. assert.equal(db.listAudit(100).some(item => item.action === 'agent_generation_fallback_created'), true);
  519. } finally {
  520. closeFixture({ service, db, dir });
  521. }
  522. }
  523. async function main() {
  524. await check('批次内单条失败不阻断后续消息并提交游标', testBatchIsolationAndCursorCommit);
  525. await check('停滞游标经过等待而不是忙循环', testStalledCursorDoesNotSpin);
  526. await check('瞬时 Agent 失败走最新草稿有限重试', testPollerGenerationRetry);
  527. await check('自动回复发送未确认时走最新草稿有限重试', testPollerAutopilotSendRetry);
  528. await check('群聊回调/轮询后台生成时不丢失同群后续消息', testGroupDeferredGenerationQueuesEveryInbound);
  529. await check('群聊自动回复在上游未确认时有限重试后再标记已发送', testGroupAutoSendRetriesWithoutFalseSent);
  530. await check('群聊自动回复失败时保留待审核草稿且不伪造已发送', testGroupAutoSendFailureKeepsPendingDraft);
  531. await check('模型重试与实时画像/需求/预警在生成期间可见', testGenerationRetryAndRealtimeIntelligence);
  532. await check('模型超时后问候与普通消息均有自动回复兜底', testGenerationFallbackAutoReply);
  533. await check('回调已确认但生成中断的消息会在重启后恢复投递', testRestartRecoversAcknowledgedGeneration);
  534. await check('风险消息的模型超时兜底保持人工审核', testRiskFallbackStaysHumanReview);
  535. process.stdout.write(`${JSON.stringify({ status: 'passed', results }, null, 2)}\n`);
  536. }
  537. main().catch(error => {
  538. process.stderr.write(`${JSON.stringify({ status: 'failed', message: error.message, stack: error.stack }, null, 2)}\n`);
  539. process.exitCode = 1;
  540. });