feat: ship deterministic parser and lifecycle release
This commit is contained in:
1 parent
208c434b31
commit
7b5d855b09
176 files changed
+20248
-1362
No files matched your search
@@ -8,6 +8,7 @@ import {
|
||||
type AgentBusTaskGateway,
|
||||
createTaskResultFrame,
|
||||
createTaskProgressFrame,
|
||||
extractAgentBusBusinessText,
|
||||
isInboundAgentBusTask,
|
||||
parseAgentBusFrame,
|
||||
sessionFromReadyFrame,
|
||||
@@ -15,7 +16,12 @@ import {
|
||||
taskResultStatus,
|
||||
taskResultText
|
||||
} from '../src/agentbus.js';
|
||||
import { mergeRuntimeChannelStatuses, type PublicAgentBusChannel } from '../src/agentbus-channels.js';
|
||||
import {
|
||||
AgentBusManager,
|
||||
mergeRuntimeChannelStatuses,
|
||||
type PublicAgentBusChannel
|
||||
} from '../src/agentbus-channels.js';
|
||||
import { resolveBusinessRoute } from '../src/business-routes.js';
|
||||
import type { AgentBusDelivery, PublicTask } from '../src/task-service.js';
|
||||
|
||||
function makeTask(status: string, overrides: Partial<PublicTask> = {}): PublicTask {
|
||||
@@ -33,6 +39,24 @@ function makeTask(status: string, overrides: Partial<PublicTask> = {}): PublicTa
|
||||
recovery_count: 0,
|
||||
session_id_present: false
|
||||
},
|
||||
parser: {
|
||||
route_id: null,
|
||||
input_contract_version: null,
|
||||
configured_mode: 'ai',
|
||||
config_revision: 0,
|
||||
authoritative_engine: null,
|
||||
engine_affinity: 'ai',
|
||||
version: null,
|
||||
fallback_reason: null,
|
||||
comparison_status: null,
|
||||
decision_id: null,
|
||||
review_status: null,
|
||||
diff_paths: [],
|
||||
field_differences: [],
|
||||
unreviewed_difference_count: 0,
|
||||
shadow_differences: [],
|
||||
reparse_count: 0
|
||||
},
|
||||
input_request: null,
|
||||
important_message: null,
|
||||
operation: null,
|
||||
@@ -109,6 +133,7 @@ function publicChannelFixture(overrides: Partial<PublicAgentBusChannel> = {}): P
|
||||
enabled: true,
|
||||
status: 'disabled',
|
||||
key_configured: true,
|
||||
deletable: true,
|
||||
last_connected_at: null,
|
||||
last_error: null,
|
||||
created_at: new Date().toISOString(),
|
||||
@@ -146,6 +171,32 @@ test('database-managed channels may use the global WebSocket URL without a globa
|
||||
assert.equal(config.AGENTBUS_WS_TOKEN, undefined);
|
||||
});
|
||||
|
||||
test('WeChat transport envelope exposes only its business text to the global parser', () => {
|
||||
const businessText = [
|
||||
'独立团单个下单',
|
||||
'发团日期:2026-11-02',
|
||||
'预订客户:示例客户',
|
||||
'产品搜索:示例产品',
|
||||
'预估人数:15+1',
|
||||
'用房数量:TWN 8,SGL 1'
|
||||
].join('\n');
|
||||
const wrapped = [
|
||||
'New WeChat message',
|
||||
'Conversation: conversation-example-1',
|
||||
`Text: ${businessText}`
|
||||
].join('\n');
|
||||
const extracted = extractAgentBusBusinessText(wrapped);
|
||||
assert.equal(extracted, businessText);
|
||||
assert.equal(resolveBusinessRoute(extracted).routeId, 'team_order_create');
|
||||
|
||||
const missingConversation = `New WeChat message\nConversation:\nText: ${businessText}`;
|
||||
const wrongTextLabel = `New WeChat message\nConversation: conversation-example-1\nBody: ${businessText}`;
|
||||
const emptyBody = 'New WeChat message\nConversation: conversation-example-1\nText: ';
|
||||
assert.equal(extractAgentBusBusinessText(missingConversation), missingConversation);
|
||||
assert.equal(extractAgentBusBusinessText(wrongTextLabel), wrongTextLabel);
|
||||
assert.equal(extractAgentBusBusinessText(emptyBody), emptyBody.trim());
|
||||
});
|
||||
|
||||
test('channel directory prefers the current listener over stale persisted disabled status', () => {
|
||||
const channel = publicChannelFixture({ status: 'disabled', last_error: '旧进程状态' });
|
||||
const connected = mergeRuntimeChannelStatuses([channel], [{
|
||||
@@ -406,6 +457,19 @@ test('AgentBus result text uses the unified important message and preserves the
|
||||
});
|
||||
assert.equal(taskResultStatus(erpFailure), 'failed');
|
||||
assert.equal(taskResultText(erpFailure), '此团还有【应收团款】账,不能取消!');
|
||||
|
||||
const uncertainWrite = makeTask('reconciliation_pending', {
|
||||
important_message: {
|
||||
kind: 'error',
|
||||
text: 'ERP 写入结果不确定:write_after_requery_mismatch',
|
||||
recorded_at: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
assert.equal(taskResultStatus(uncertainWrite), 'failed');
|
||||
assert.equal(
|
||||
taskResultText(uncertainWrite),
|
||||
'ERP 写入已发起,但系统尚未确认最终结果。请勿重复提交同一任务,等待管理员只读核验。'
|
||||
);
|
||||
});
|
||||
|
||||
test('AgentBus attachment references omit archived visitor XLS and keep only XLSX delivery', () => {
|
||||
@@ -564,16 +628,27 @@ test('AgentBus listener connects with the documented Authorization header and re
|
||||
from: 'channel:wechat:user-1',
|
||||
payload: { text: '格式不符合监听契约' }
|
||||
}));
|
||||
const wrappedBusinessText = [
|
||||
'New WeChat message',
|
||||
'Conversation: conversation-wechat-1',
|
||||
'Text: 独立团单个下单',
|
||||
'发团日期:2026-11-02',
|
||||
'预订客户:示例客户',
|
||||
'产品搜索:示例产品',
|
||||
'预估人数:15+1',
|
||||
'用房数量:TWN 8,SGL 1'
|
||||
].join('\n');
|
||||
socket.emit('message', JSON.stringify({
|
||||
id: 'channel-event-1',
|
||||
type: 'event',
|
||||
from: 'channel:wechat:user-1',
|
||||
payload: { text: '创建一个新团', reply_policy: { progress: true } }
|
||||
payload: { text: wrappedBusinessText, reply_policy: { progress: true } }
|
||||
}));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
assert.equal(received.length, 1);
|
||||
assert.equal(capturedContext.source, 'agentbus');
|
||||
assert.equal(received[0].message, wrappedBusinessText.split('\n').slice(2).join('\n').replace(/^Text:\s*/, ''));
|
||||
assert.equal(received[0].conversationId, 'agentbus:channel:wechat:user-1');
|
||||
assert.equal(received[0].idempotencyKey, 'agentbus:channel-event-1');
|
||||
assert.equal(socket.sent.length, 2);
|
||||
@@ -593,7 +668,9 @@ test('AgentBus listener connects with the documented Authorization header and re
|
||||
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'session_ready'));
|
||||
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'frame_ignored' && entry.metadata.ignore_reason === 'unexpected_type:message'));
|
||||
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'frame_received' && entry.metadata.frame_id === 'channel-event-1'));
|
||||
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'frame_received' && entry.metadata.text_preview === '创建一个新团'));
|
||||
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'frame_received' && entry.metadata.text_preview === wrappedBusinessText));
|
||||
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'task_processing_started'
|
||||
&& entry.metadata.transport_envelope_unwrapped === true));
|
||||
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'task_ingested' && entry.metadata.task_id === 'TASK-1'));
|
||||
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'outbound_progress_sent'));
|
||||
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'outbound_result_sent'));
|
||||
@@ -617,6 +694,40 @@ test('listener reload stop does not overwrite the channel status as disabled', (
|
||||
assert.deepEqual(statuses, ['disabled']);
|
||||
});
|
||||
|
||||
test('channel manager replays a reload requested while another reload is in flight', async () => {
|
||||
const manager = new AgentBusManager({
|
||||
config: testConfig(),
|
||||
tasks: {} as AgentBusTaskGateway,
|
||||
organizationId: 'org-1',
|
||||
scheduleParseQueue: async () => {}
|
||||
});
|
||||
const service = manager.channelService as unknown as {
|
||||
ensureLegacyChannel: (organizationId: string) => Promise<void>;
|
||||
listEnabledSecrets: (organizationId: string) => Promise<[]>;
|
||||
};
|
||||
let listCalls = 0;
|
||||
let releaseFirstList: (() => void) | undefined;
|
||||
service.ensureLegacyChannel = async () => {};
|
||||
service.listEnabledSecrets = async () => {
|
||||
listCalls += 1;
|
||||
if (listCalls === 1) {
|
||||
await new Promise<void>((resolve) => {
|
||||
releaseFirstList = resolve;
|
||||
});
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const initialReload = manager.start();
|
||||
while (!releaseFirstList) await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
const mutationReload = manager.reload();
|
||||
releaseFirstList();
|
||||
await Promise.all([initialReload, mutationReload]);
|
||||
|
||||
assert.equal(listCalls, 2);
|
||||
await manager.stop();
|
||||
});
|
||||
|
||||
test('listener shutdown carries its session epoch for stale-writer protection', () => {
|
||||
const socket = new FakeSocket();
|
||||
const changes: Array<{ status: string; epoch: number | null }> = [];
|
||||
|
||||
Reference in new issue
Block a user