feat: add message source context enhancement

This commit is contained in:
xmanrui 2026-08-29 10:46:06 +08:00
parent 5928b33057
commit 0cb79f74e9
69 changed files with 6217 additions and 597 deletions

View file

@ -226,6 +226,84 @@ test('runtime sends visible-scope messages to Harness without local sender appro
await runtime.stop();
});
for (const scenario of [
{ name: 'private enabled to disabled', conversationType: '1', before: [true, false], after: [false, true] },
{ name: 'group enabled to disabled', conversationType: '2', before: [false, true], after: [true, false] },
{ name: 'private disabled to enabled', conversationType: '1', before: [false, true], after: [true, false] },
]) {
test(`DingTalk received callback retains settings before its parse microtask: ${scenario.name}`, async () => {
const order = [];
const asked = [];
const configFor = ([directEnabled, groupEnabled], guidance) => ({
directEnabled, groupEnabled, fields: ['channel', 'botId'], guidance,
});
let config = configFor(scenario.before, 'before callback returned');
let callback;
const client = {
connected: true,
socket: { readyState: 1 },
registerCallbackListener(_topic, listener) { callback = listener; },
async connect() {},
socketCallBackResponse(messageId) { order.push(['ack', messageId]); },
disconnect() {},
};
const runtime = new DingtalkRuntime({
config: { clientId: 'ding-client', approvedSenders: [] },
clientSecret: 'host-secret',
contextEnhancement: {
botId: 'dingtalk_internal',
getSettings: () => { order.push(['settings']); return config; },
},
harness: {
ensureRunning: async () => true,
createSession: async () => 'session-existing',
sessionExists: async () => true,
ask: async (_sessionId, text) => { asked.push(text); order.push(['ask']); return 'done'; },
},
state: stateFixture(),
api: { sendText: async () => {} },
streamFactory: async () => ({ client, topic: 'robot-topic' }),
logger: { warn() {}, error() {} },
});
const response = (id) => ({
headers: { messageId: `callback-${id}` },
get data() {
order.push(['read-data', id]);
return JSON.stringify({
msgId: `message-${id}`, msgtype: 'text', text: { content: `message ${id}` },
conversationType: scenario.conversationType, conversationId: 'group-chat',
isInAtList: true, senderStaffId: 'staff-id', senderNick: 'Ada',
sessionWebhook: 'https://oapi.dingtalk.com/robot/reply?ticket=test',
});
},
});
try {
await runtime.start();
callback(response(1));
assert.deepEqual(order, [['ack', 'callback-1'], ['settings']], 'ACK stays first and JSON remains unparsed until the original microtask');
config = configFor(scenario.after, 'after callback returned');
await eventually(() => runtime.status.messagesReplied === 1, 5_000);
callback(response(2));
await eventually(() => runtime.status.messagesReplied === 2, 5_000);
for (const [index, switches] of [scenario.before, scenario.after].entries()) {
const enabled = switches[scenario.conversationType === '1' ? 0 : 1];
if (enabled) {
assert.match(asked[index], index === 0 ? /before callback returned/ : /after callback returned/);
assert.deepEqual(JSON.parse(/^<dsh_im_source>(.*?)<\/dsh_im_source>/su.exec(asked[index])[1]), {
channel: 'dingtalk', botId: 'dingtalk_internal',
});
} else {
assert.equal(asked[index], `message ${index + 1}`);
}
}
assert.equal(order.filter(([action]) => action === 'settings').length, 2, 'one source configuration read per callback');
assert.equal(order.filter(([action]) => action === 'read-data').length, 4, 'preserves the original data parsing path');
} finally {
await runtime.stop();
}
});
}
test('runtime accepts an OPEN DingTalk socket when the SDK registered flag remains false', async () => {
const client = {
connected: true,