| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- 'use strict';
- const assert = require('assert/strict');
- const fs = require('fs');
- const os = require('os');
- const path = require('path');
- const { AgentWorkbenchDb } = require('../mcp/src/core/agent-workbench-db');
- const { AgentWorkbenchService } = require('../mcp/src/core/agent-workbench-service');
- const results = [];
- function deferred() {
- let resolve;
- let reject;
- const promise = new Promise((resolvePromise, rejectPromise) => {
- resolve = resolvePromise;
- reject = rejectPromise;
- });
- return { promise, resolve, reject };
- }
- function qualifiedOutput(content, profileUpdates = {}) {
- return {
- content,
- confidence: 0.96,
- intent: 'next_step',
- reason: 'Deterministic concurrency fixture.',
- requiresHuman: false,
- profileUpdates,
- tasks: [],
- alerts: [],
- citations: [],
- toolTrace: [],
- };
- }
- function setup({ mode = 'review', agentRun, journeyDefinition = null } = {}) {
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-generation-concurrency-'));
- const db = new AgentWorkbenchDb(path.join(dir, 'workbench.db'), {
- defaultMode: mode,
- autoSendConfidence: 0.8,
- });
- const sent = [];
- const service = new AgentWorkbenchService({
- db,
- agent: {
- modelClient: { isConfigured: () => true },
- run: agentRun,
- },
- qiwei: {
- isConfigured: () => true,
- async sendText(toId, content) {
- sent.push({ toId, content });
- return { isSendSuccess: true };
- },
- },
- config: {
- accountKey: 'generation-concurrency-smoke',
- agent: {
- apiKey: 'fixture',
- model: 'fixture',
- provider: 'fixture',
- qualityPassScore: 82,
- journeyDefinition,
- },
- qiwei: { allowedSenders: ['contact-1', 'contact-2'] },
- memory: { enabled: false },
- },
- });
- return {
- dir,
- db,
- sent,
- service,
- close() {
- service.stopBackgroundWorkers();
- db.close();
- fs.rmSync(dir, { recursive: true, force: true });
- },
- };
- }
- async function check(name, fn) {
- await fn();
- results.push({ name, status: 'passed' });
- }
- async function runStaleMode(mode) {
- const firstStarted = deferred();
- const firstReply = deferred();
- let calls = 0;
- let active = 0;
- let maxActive = 0;
- const ctx = setup({
- mode,
- agentRun: async input => {
- calls += 1;
- active += 1;
- maxActive = Math.max(maxActive, active);
- try {
- if (calls === 1) {
- firstStarted.resolve();
- await firstReply.promise;
- return qualifiedOutput('第一条旧回复不应落库;建议忽略该结果。', { staleField: 'must-not-persist' });
- }
- assert.match(input.inboundContent, /第二条/);
- return qualifiedOutput('第二条信息已作为当前依据;建议按最新内容继续推进。', { latestField: 'persisted' });
- } finally {
- active -= 1;
- }
- },
- });
- try {
- const first = ctx.service.ingestInbound({
- externalId: `${mode}-first`,
- contactId: 'contact-1',
- content: '第一条问题,请给建议。',
- });
- await firstStarted.promise;
- const second = ctx.service.ingestInbound({
- externalId: `${mode}-second`,
- contactId: 'contact-1',
- content: '第二条补充,请按最新内容处理。',
- });
- assert.equal(ctx.sent.length, 0);
- firstReply.resolve();
- const firstResult = await first;
- const secondResult = await second;
- assert.equal(firstResult.status, 'stale_discarded');
- assert.equal(secondResult.status, mode === 'review' ? 'pending_review' : mode === 'auto' ? 'sent' : 'autopilot_sent');
- assert.equal(calls, 2);
- assert.equal(maxActive, 1);
- assert.equal(ctx.sent.length, mode === 'review' ? 0 : 1);
- const conversation = ctx.db.getConversationByContactId('contact-1');
- const profile = ctx.db.getProfile(conversation.id).profile;
- assert.equal(profile.staleField, undefined);
- assert.equal(profile.latestField, 'persisted');
- const drafts = ctx.db.listDrafts({ conversationId: conversation.id });
- assert.equal(drafts.some(draft => draft.inbound_message_id === firstResult.message.id), false);
- assert.equal(ctx.db.listConversationAppliedActions(conversation.id).some(action => (
- action.payload?.inboundMessageId === firstResult.message.id
- )), false);
- assert.equal(ctx.db.listAudit(100, conversation.id).filter(item => item.action === 'agent_response_stale_discarded').length, 1);
- } finally {
- ctx.close();
- }
- }
- async function main() {
- for (const mode of ['review', 'auto', 'autopilot']) {
- await check(`${mode} discards a pending stale generation and serializes the next turn`, () => runStaleMode(mode));
- }
- await check('direct autopilot and automatic approval reject superseded inbound messages', async () => {
- const ctx = setup({ mode: 'autopilot', agentRun: async () => qualifiedOutput('当前回复可执行;建议继续推进。') });
- try {
- const conversation = ctx.db.ensureConversation('contact-1', 'Fixture');
- const oldInbound = ctx.db.insertMessage({
- conversationId: conversation.id,
- direction: 'inbound',
- senderType: 'customer',
- content: '旧问题',
- }).message;
- const staleDraft = ctx.db.createDraft({
- conversationId: conversation.id,
- inboundMessageId: oldInbound.id,
- content: '旧草稿',
- confidence: 0.99,
- requiresHuman: false,
- citations: [],
- toolTrace: [],
- });
- ctx.db.insertMessage({
- conversationId: conversation.id,
- direction: 'inbound',
- senderType: 'customer',
- content: '新问题',
- });
- await assert.rejects(() => ctx.service.sendAutopilotReply(conversation, oldInbound, {
- content: '旧自动回复',
- qualityPassed: true,
- requiresHuman: false,
- }), error => error.code === 'AGENT_RESPONSE_STALE');
- const autoApproval = await ctx.service.approveDraft(staleDraft.id, { actor: 'agent:auto' });
- assert.equal(autoApproval.status, 'stale_discarded');
- assert.equal(autoApproval.draft.status, 'rejected');
- assert.equal(ctx.sent.length, 0);
- } finally {
- ctx.close();
- }
- });
- await check('human approval remains available for an intentionally reviewed older draft', async () => {
- const ctx = setup({ mode: 'review', agentRun: async () => qualifiedOutput('当前回复可执行;建议继续推进。') });
- try {
- const conversation = ctx.db.ensureConversation('contact-1', 'Fixture');
- const oldInbound = ctx.db.insertMessage({ conversationId: conversation.id, direction: 'inbound', senderType: 'customer', content: '旧问题' }).message;
- const draft = ctx.db.createDraft({
- conversationId: conversation.id,
- inboundMessageId: oldInbound.id,
- content: '人工确认后的旧问题答复',
- confidence: 0.1,
- requiresHuman: true,
- citations: [],
- toolTrace: [],
- });
- ctx.db.insertMessage({ conversationId: conversation.id, direction: 'inbound', senderType: 'customer', content: '新问题' });
- const result = await ctx.service.approveDraft(draft.id, { actor: 'human', content: '人工明确选择发送的内容' });
- assert.equal(result.status, 'sent');
- assert.equal(ctx.sent.length, 1);
- } finally {
- ctx.close();
- }
- });
- await check('journey remains planned for a draft and advances only after a real send', async () => {
- const journeyDefinition = {
- id: 'generic-service-flow',
- initialStageId: 'understand',
- stages: [
- { id: 'understand', requiredFacts: ['goal'], nextStageId: 'qualify' },
- { id: 'qualify', requiredFacts: ['budget'], nextStageId: 'act' },
- { id: 'act', requiredFacts: [], nextStageId: null },
- ],
- };
- const ctx = setup({
- mode: 'review',
- journeyDefinition,
- agentRun: async () => qualifiedOutput(
- '可以围绕当前目标继续推进;建议先整理一个可执行方案。',
- { goal: 'improve service', budget: 'confirmed range' },
- ),
- });
- try {
- const generated = await ctx.service.ingestInbound({
- externalId: 'journey-message',
- contactId: 'contact-1',
- content: '目标和投入范围都明确了,请给下一步建议。',
- });
- assert.equal(generated.status, 'pending_review');
- let journey = ctx.db.getConversationJourney(generated.conversation.id);
- assert.equal(journey.currentStageId, 'understand');
- assert.equal(journey.status, 'active');
- assert.equal(journey.appliedActions.length, 1);
- assert.equal(journey.appliedActions[0].status, 'planned');
- await ctx.service.approveDraft(generated.draft.id, { actor: 'human' });
- journey = ctx.db.getConversationJourney(generated.conversation.id);
- assert.equal(journey.currentStageId, 'act');
- assert.equal(journey.status, 'completed');
- assert.equal(journey.appliedActions[0].status, 'confirmed');
- assert.equal(journey.appliedActions[0].confirmationEvidence.type, 'send');
- assert.deepEqual(journey.state.stageHistory.filter(item => item.kind === 'skip').map(item => item.stageId), ['qualify']);
- } finally {
- ctx.close();
- }
- });
- process.stdout.write(`${JSON.stringify({ status: 'ok', checks: results.length, results }, null, 2)}\n`);
- }
- main().catch(error => {
- console.error(error);
- process.exitCode = 1;
- });
|