mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 05:23:18 +08:00
240 lines
7.4 KiB
JavaScript
240 lines
7.4 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { FeishuHarnessBridge } from '../../../src/channels/feishu/bridge.mjs';
|
|
|
|
function event(messageId, text) {
|
|
return {
|
|
sender: { sender_type: 'user', sender_id: { open_id: 'ou_user' } },
|
|
message: {
|
|
message_id: messageId,
|
|
message_type: 'text',
|
|
chat_type: 'p2p',
|
|
chat_id: 'oc_chat',
|
|
content: JSON.stringify({ text }),
|
|
},
|
|
};
|
|
}
|
|
|
|
test('bridge maps a Feishu conversation to a persistent Harness session and replies', async () => {
|
|
const sent = [];
|
|
const reactions = [];
|
|
const removedReactions = [];
|
|
const streamed = [];
|
|
const sessions = new Map();
|
|
const seen = new Set();
|
|
const asked = [];
|
|
const client = {
|
|
im: { v1: { message: { create: async (request) => {
|
|
sent.push(request);
|
|
return { code: 0 };
|
|
} } } },
|
|
};
|
|
const channel = {
|
|
addReaction: async (messageId, emojiType) => {
|
|
reactions.push({ messageId, emojiType });
|
|
return `reaction-${emojiType}`;
|
|
},
|
|
removeReaction: async (messageId, reactionId) => {
|
|
removedReactions.push({ messageId, reactionId });
|
|
},
|
|
stream: async (chatId, input, options) => {
|
|
const updates = [];
|
|
await input.markdown({
|
|
setContent: async (content) => updates.push(content),
|
|
});
|
|
streamed.push({ chatId, options, updates });
|
|
return { messageId: 'om_reply' };
|
|
},
|
|
};
|
|
const harness = {
|
|
ensureRunning: async () => true,
|
|
sessionExists: async (sessionId) => sessionId === 'session-test',
|
|
createSession: async () => 'session-test',
|
|
ask: async (sessionId, text, options) => {
|
|
asked.push({ sessionId, text });
|
|
await options.onUpdate({ type: 'text', text: 'Harness' });
|
|
return 'Harness reply';
|
|
},
|
|
};
|
|
const state = {
|
|
hasSeen: (id) => seen.has(id),
|
|
markSeen: async (id) => seen.add(id),
|
|
sessionFor: (key) => sessions.get(key) ?? null,
|
|
setSession: async (key, sessionId) => sessions.set(key, sessionId),
|
|
clearSession: async (key) => sessions.delete(key),
|
|
};
|
|
const status = {
|
|
messagesReceived: 0,
|
|
messagesReplied: 0,
|
|
messagesRejected: 0,
|
|
lastMessageAt: null,
|
|
lastReplyAt: null,
|
|
lastRejectedAt: null,
|
|
lastError: null,
|
|
};
|
|
const bridge = new FeishuHarnessBridge({
|
|
client,
|
|
channel,
|
|
harness,
|
|
state,
|
|
status,
|
|
allowedSenderOpenIds: new Set(['ou_user']),
|
|
});
|
|
|
|
bridge.accept(event('om_1', '你好'));
|
|
await bridge.waitForIdle();
|
|
|
|
assert.equal(sessions.get('p2p:ou_user'), 'session-test');
|
|
assert.deepEqual(asked, [{ sessionId: 'session-test', text: '你好' }]);
|
|
assert.deepEqual(streamed, [{
|
|
chatId: 'oc_chat',
|
|
options: { replyTo: 'om_1' },
|
|
updates: ['Harness', 'Harness reply'],
|
|
}]);
|
|
assert.deepEqual(reactions, [
|
|
{ messageId: 'om_1', emojiType: 'OnIt' },
|
|
{ messageId: 'om_1', emojiType: 'DONE' },
|
|
]);
|
|
assert.deepEqual(removedReactions, [
|
|
{ messageId: 'om_1', reactionId: 'reaction-OnIt' },
|
|
]);
|
|
assert.equal(sent.length, 0);
|
|
assert.equal(status.messagesReceived, 1);
|
|
assert.equal(status.messagesReplied, 1);
|
|
assert.equal(status.streamResponses, 1);
|
|
|
|
bridge.accept(event('om_1', '重复消息'));
|
|
await bridge.waitForIdle();
|
|
assert.equal(asked.length, 1);
|
|
|
|
bridge.accept({
|
|
...event('om_2', '越权消息'),
|
|
sender: { sender_type: 'user', sender_id: { open_id: 'ou_other' } },
|
|
});
|
|
await bridge.waitForIdle();
|
|
assert.equal(asked.length, 1);
|
|
assert.equal(status.messagesRejected, 1);
|
|
});
|
|
|
|
test('reaction failures do not block streaming replies', async () => {
|
|
const seen = new Set();
|
|
const status = { messagesReceived: 0, messagesReplied: 0, messagesRejected: 0 };
|
|
const bridge = new FeishuHarnessBridge({
|
|
client: {},
|
|
channel: {
|
|
addReaction: async () => { throw new Error('reaction unavailable'); },
|
|
stream: async (_chatId, input) => input.markdown({ setContent: async () => undefined }),
|
|
},
|
|
harness: {
|
|
sessionExists: async () => true,
|
|
ask: async (_sessionId, _text, options) => {
|
|
await options.onUpdate({ type: 'tool', name: 'web_search' });
|
|
return '天气结果';
|
|
},
|
|
},
|
|
state: {
|
|
hasSeen: (id) => seen.has(id),
|
|
markSeen: async (id) => seen.add(id),
|
|
sessionFor: () => 'session-existing',
|
|
},
|
|
status,
|
|
allowedSenderOpenIds: new Set(['ou_user']),
|
|
});
|
|
|
|
bridge.accept(event('om_reaction_failure', '深圳天气'));
|
|
await bridge.waitForIdle();
|
|
|
|
assert.equal(status.messagesReplied, 1);
|
|
assert.equal(status.reactionErrors, 2);
|
|
assert.equal(status.streamResponses, 1);
|
|
});
|
|
|
|
test('a stream finalization failure falls back to text without repeating the prompt', async () => {
|
|
const seen = new Set();
|
|
const sent = [];
|
|
const finalReactions = [];
|
|
let askCount = 0;
|
|
const status = { messagesReceived: 0, messagesReplied: 0, messagesRejected: 0 };
|
|
const bridge = new FeishuHarnessBridge({
|
|
client: {
|
|
im: { v1: { message: { create: async (request) => {
|
|
sent.push(JSON.parse(request.data.content).text);
|
|
return { code: 0 };
|
|
} } } },
|
|
},
|
|
channel: {
|
|
addReaction: async (_messageId, emojiType) => {
|
|
finalReactions.push(emojiType);
|
|
return `reaction-${emojiType}`;
|
|
},
|
|
removeReaction: async () => undefined,
|
|
stream: async (_chatId, input) => {
|
|
await input.markdown({ setContent: async () => undefined });
|
|
throw new Error('card finalization failed');
|
|
},
|
|
},
|
|
harness: {
|
|
sessionExists: async () => true,
|
|
ask: async () => {
|
|
askCount += 1;
|
|
return '已经生成的最终回答';
|
|
},
|
|
},
|
|
state: {
|
|
hasSeen: (id) => seen.has(id),
|
|
markSeen: async (id) => seen.add(id),
|
|
sessionFor: () => 'session-existing',
|
|
},
|
|
status,
|
|
allowedSenderOpenIds: new Set(['ou_user']),
|
|
});
|
|
|
|
bridge.accept(event('om_stream_finalize_failure', '不要重复提交'));
|
|
await bridge.waitForIdle();
|
|
|
|
assert.equal(askCount, 1);
|
|
assert.deepEqual(sent, ['已经生成的最终回答']);
|
|
assert.deepEqual(finalReactions, ['OnIt', 'DONE']);
|
|
assert.equal(status.messagesReplied, 1);
|
|
assert.equal(status.streamFallbacks, 1);
|
|
assert.equal(status.streamErrors, 1);
|
|
});
|
|
|
|
test('bridge does not expose internal error details in a Feishu failure reply', async () => {
|
|
const sent = [];
|
|
const seen = new Set();
|
|
const status = { messagesReceived: 0, messagesReplied: 0, messagesRejected: 0 };
|
|
const bridge = new FeishuHarnessBridge({
|
|
client: {
|
|
im: { v1: { message: { create: async (request) => {
|
|
sent.push(JSON.parse(request.data.content).text);
|
|
return { code: 0 };
|
|
} } } },
|
|
},
|
|
channel: {
|
|
addReaction: async (_messageId, emojiType) => `reaction-${emojiType}`,
|
|
removeReaction: async () => undefined,
|
|
},
|
|
harness: {
|
|
sessionExists: async () => true,
|
|
ask: async () => {
|
|
throw new Error('secret-shaped-internal-detail /private/path');
|
|
},
|
|
},
|
|
state: {
|
|
hasSeen: (id) => seen.has(id),
|
|
markSeen: async (id) => seen.add(id),
|
|
sessionFor: () => 'session-existing',
|
|
},
|
|
status,
|
|
allowedSenderOpenIds: new Set(['ou_user']),
|
|
});
|
|
|
|
bridge.accept(event('om_internal_failure', '触发错误'));
|
|
await bridge.waitForIdle();
|
|
|
|
assert.equal(sent.length, 1);
|
|
assert.match(sent[0], /处理失败,请稍后重试/);
|
|
assert.doesNotMatch(sent[0], /secret-shaped-internal-detail|private\/path/);
|
|
assert.equal(status.lastError, 'secret-shaped-internal-detail /private/path');
|
|
});
|