import test from 'node:test'; import assert from 'node:assert/strict'; import { FeishuHarnessBridge } from '../../../src/channels/feishu/bridge.mjs'; function deferred() { let resolve; let reject; const promise = new Promise((promiseResolve, promiseReject) => { resolve = promiseResolve; reject = promiseReject; }); return { promise, resolve, reject }; } async function eventually(predicate, message = 'condition was not met') { const deadline = Date.now() + 1_000; while (Date.now() < deadline) { if (predicate()) return; await new Promise((resolve) => setTimeout(resolve, 5)); } assert.fail(message); } function event(messageId, text, overrides = {}) { const { senderOpenId = 'ou_user', ...messageOverrides } = overrides; return { sender: { sender_type: 'user', sender_id: { open_id: senderOpenId } }, message: { message_id: messageId, message_type: 'text', chat_type: 'p2p', chat_id: 'oc_chat', content: JSON.stringify({ text }), ...messageOverrides, }, }; } function stateFixture(initialSessions = []) { const sessions = new Map(initialSessions); const seen = new Set(); return { sessions, seen, 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), }, }; } function bridgeStatus() { return { messagesReceived: 0, messagesReplied: 0, messagesRejected: 0, lastMessageAt: null, lastReplyAt: null, lastRejectedAt: null, lastError: null, }; } function textClient(sendText) { let sequence = 0; return { im: { v1: { message: { create: async (request) => { const outgoing = { chatId: request.data.receive_id, text: JSON.parse(request.data.content).text, }; await sendText(outgoing); sequence += 1; return { code: 0, data: { message_id: `om_test_${sequence}` } }; } } } }, }; } 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('a threaded Feishu reply answers a pending Harness question before the original turn queue', async () => { const sent = []; const streamed = []; const asked = []; const seen = new Set(); const sessions = new Map([['p2p:ou_user', 'session-question']]); const submitStarted = deferred(); const releaseSubmit = deferred(); const answerAccepted = deferred(); let originalTurnFinished = false; const status = { messagesReceived: 0, messagesReplied: 0, messagesRejected: 0, lastMessageAt: null, lastReplyAt: null, lastRejectedAt: null, lastError: null, }; const bridge = new FeishuHarnessBridge({ client: { im: { v1: { message: { create: async (request) => { sent.push(JSON.parse(request.data.content).text); return { code: 0, data: { message_id: `om_sent_${sent.length}` } }; } } } }, }, channel: { stream: async (_chatId, input) => { await input.markdown({ setContent: async (content) => streamed.push(content), }); originalTurnFinished = true; return { messageId: 'om_stream' }; }, }, harness: { ensureRunning: async () => true, sessionExists: async () => true, createSession: async () => assert.fail('the existing session should be reused'), ask: async (sessionId, text, options) => { asked.push({ sessionId, text }); await options.onUpdate({ type: 'tool', name: 'ask_user_question' }); await options.onInteraction({ kind: 'question', interactionId: 'question-rpc', rpcId: 'question-rpc', sessionId, payload: { type: 'question/requested', sessionId, questions: [{ id: 'environment', header: '测试环境', question: '请选择测试环境', options: [{ label: '测试环境' }, { label: '生产环境' }], }], }, respond: async (result) => { submitStarted.resolve(result); await releaseSubmit.promise; answerAccepted.resolve(); return { accepted: true }; }, }); await answerAccepted.promise; return '你选择了:测试环境'; }, }, 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), }, status, allowedSenderOpenIds: new Set(['ou_user']), }); bridge.accept(event('om_prompt', '请先调用 ask_user_question')); await eventually( () => [...sent, ...streamed].some((text) => text.includes('请选择测试环境')), 'the Harness question was not presented in Feishu', ); bridge.accept(event('om_answer', '1', { root_id: 'om_prompt', parent_id: 'om_sent_1', thread_id: 'omt_question_thread', })); const submitted = await Promise.race([ submitStarted.promise, new Promise((_, reject) => setTimeout( () => reject(new Error('threaded Feishu answer deadlocked behind the original turn')), 500, )), ]); assert.equal(originalTurnFinished, false); assert.deepEqual(submitted, { ok: true, value: { sessionId: 'session-question', answer: { answers: [{ id: 'environment', selected: ['测试环境'] }], }, }, }); assert.deepEqual(asked, [{ sessionId: 'session-question', text: '请先调用 ask_user_question', }]); // This matches the screenshot: /status can arrive while the answer is being // submitted. It may wait for the original turn, but it must not remain stuck. bridge.accept(event('om_status', '/status')); releaseSubmit.resolve(); await bridge.waitForIdle(); assert.equal(originalTurnFinished, true); assert.equal(streamed.at(-1), '你选择了:测试环境'); assert.equal(sent.some((text) => text.includes('连接正常')), true); assert.deepEqual(asked, [{ sessionId: 'session-question', text: '请先调用 ask_user_question', }], 'the answer and /status must not become new Harness prompts'); assert.deepEqual([...seen].sort(), ['om_answer', 'om_prompt', 'om_status']); assert.equal(status.messagesReceived, 3); assert.equal(status.messagesReplied, 1); }); test('pending Harness questions are isolated by Feishu conversation', async () => { const fixture = stateFixture([ ['p2p:ou_a', 'session-a'], ['p2p:ou_b', 'session-b'], ]); const sent = []; const asked = []; const answeredA = deferred(); const releaseA = deferred(); const bridge = new FeishuHarnessBridge({ client: textClient(async (message) => sent.push(message)), harness: { sessionExists: async () => true, createSession: async () => assert.fail('existing sessions should be reused'), ask: async (sessionId, text, options) => { asked.push({ sessionId, text }); if (sessionId === 'session-b') return '乙会话的普通回答'; await options.onInteraction({ kind: 'question', interactionId: 'question-a', rpcId: 'question-a', sessionId, payload: { type: 'question/requested', sessionId, questions: [{ id: 'a', question: '甲会话的问题' }], }, respond: async (result) => { answeredA.resolve(result); return { accepted: true }; }, }); await answeredA.promise; await releaseA.promise; return '甲会话完成'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_a', 'ou_b']), }); const firstA = bridge.accept(event('a-prompt', '启动甲会话', { senderOpenId: 'ou_a', chat_id: 'oc_a', })); await eventually(() => sent.some(({ text }) => text.includes('甲会话的问题'))); await bridge.accept(event('b-message', '乙会话的消息', { senderOpenId: 'ou_b', chat_id: 'oc_b', })); assert.deepEqual(asked, [ { sessionId: 'session-a', text: '启动甲会话' }, { sessionId: 'session-b', text: '乙会话的消息' }, ]); assert.equal(sent.some(({ chatId, text }) => ( chatId === 'oc_b' && text === '乙会话的普通回答' )), true); await bridge.accept(event('a-answer', '甲的答案', { senderOpenId: 'ou_a', chat_id: 'oc_a', })); assert.deepEqual((await answeredA.promise).value.answer.answers, [ { id: 'a', selected: [], custom: '甲的答案' }, ]); releaseA.resolve(); await firstA; }); test('Feishu handles approval replies on the fast lane and presents approvals in FIFO order', async () => { const fixture = stateFixture([['p2p:ou_user', 'session-approval']]); const sent = []; const asked = []; const decisions = []; const decided = deferred(); const bridge = new FeishuHarnessBridge({ client: textClient(async (message) => sent.push(message)), harness: { sessionExists: async () => true, createSession: async () => assert.fail('the existing session should be reused'), ask: async (sessionId, text, options) => { asked.push({ sessionId, text }); const approval = (approvalId, toolName, reason) => ({ kind: 'approval', interactionId: approvalId, rpcId: `rpc-${approvalId}`, sessionId, payload: { type: 'approval/requested', sessionId, approvalId, toolName, callId: `call-${approvalId}`, reason, }, toolCall: { callId: `call-${approvalId}`, name: toolName, arguments: JSON.stringify({ operation: reason }), }, respond: async (result) => { decisions.push(result); if (decisions.length === 2) decided.resolve(); return { accepted: true }; }, }); await options.onInteraction(approval( 'approval-build', 'bash', '运行第一项构建操作', )); await options.onInteraction(approval( 'approval-write', 'write_file', '运行第二项写入操作', )); await decided.promise; return '两个审批均已处理'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_user']), }); const turn = bridge.accept(event('approval-start', '发起两个审批')); await eventually(() => sent.some(({ text }) => text.includes('运行第一项构建操作'))); assert.equal(sent.some(({ text }) => text.includes('运行第二项写入操作')), false); assert.equal(sent.some(({ text }) => text.includes('approval-build')), false); await bridge.accept(event('approval-invalid', '好的')); assert.deepEqual(decisions, []); assert.deepEqual(asked, [{ sessionId: 'session-approval', text: '发起两个审批' }]); assert.match(sent.at(-1).text, /批准/); assert.match(sent.at(-1).text, /拒绝/); await bridge.accept(event('approval-allow', '批准')); assert.deepEqual(decisions, [{ ok: true, value: { sessionId: 'session-approval', approvalId: 'approval-build', outcome: 'allowed-once', }, }]); assert.equal(sent.filter(({ text }) => text.includes('运行第二项写入操作')).length, 1); assert.equal(sent.some(({ text }) => text.includes('approval-write')), false); await bridge.accept(event('approval-reject', '拒绝')); await turn; assert.deepEqual(decisions, [ { ok: true, value: { sessionId: 'session-approval', approvalId: 'approval-build', outcome: 'allowed-once', }, }, { ok: true, value: { sessionId: 'session-approval', approvalId: 'approval-write', outcome: 'rejected', }, }, ]); assert.equal(sent.at(-1).text, '两个审批均已处理'); }); test('question replays are deduplicated and an unrenderable approval is safely rejected', async () => { const fixture = stateFixture(); const sent = []; let approvalResponse; let parallelQuestionResponse; const bridge = new FeishuHarnessBridge({ client: textClient(async (message) => sent.push(message)), harness: { sessionExists: async () => false, createSession: async () => 'session-replay', ask: async (sessionId, _text, options) => { const question = { kind: 'question', interactionId: 'replayed-question', rpcId: 'replayed-question', sessionId, payload: { type: 'question/requested', sessionId, questions: [{ id: 'choice', question: '只应显示一次' }], }, respond: async () => ({ accepted: true }), }; await options.onInteraction(question); await options.onInteraction(question); await options.onInteraction({ kind: 'question', interactionId: 'parallel-question', rpcId: 'parallel-question', sessionId, payload: { type: 'question/requested', sessionId, questions: [{ id: 'parallel', question: '不应无声丢弃' }], }, respond: async (result) => { parallelQuestionResponse = result; return { accepted: true }; }, }); await options.onInteraction({ kind: 'approval', interactionId: 'approval-one', rpcId: 'approval-rpc', sessionId, payload: { type: 'approval/requested', sessionId, approvalId: 'approval-one', toolName: 'bash', }, respond: async (result) => { approvalResponse = result; return { accepted: true }; }, }); await options.onInteractionResolved({ kind: 'question', sessionId, interactionId: 'replayed-question', outcome: 'cancelled', }); return '交互已取消'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_user']), logger: { info() {}, warn() {}, error() {} }, }); await bridge.accept(event('replay', '测试重放')); assert.equal(sent.filter(({ text }) => text.includes('只应显示一次')).length, 1); assert.deepEqual(parallelQuestionResponse, { ok: false, error: { code: 'cancelled', message: 'Feishu is already handling another user interaction.', details: {}, }, }); assert.deepEqual(approvalResponse, { ok: true, value: { sessionId: 'session-replay', approvalId: 'approval-one', outcome: 'rejected', }, }); assert.equal(sent.some(({ text }) => text.includes('无法完整展示')), true); assert.equal(sent.at(-1).text, '交互已取消'); }); test('a queued next prompt stays separate while a failed interaction response is retried', async () => { const fixture = stateFixture([['p2p:ou_user', 'session-submit-retry']]); const sent = []; const asked = []; const firstSubmitStarted = deferred(); const releaseFirstSubmit = deferred(); const answered = deferred(); const submittedAnswers = []; let submitAttempts = 0; const bridge = new FeishuHarnessBridge({ client: textClient(async (message) => sent.push(message)), harness: { sessionExists: async () => true, createSession: async () => assert.fail('the existing session should be reused'), ask: async (sessionId, text, options) => { asked.push(text); if (text === '排队的下一个问题') return '第二轮完成'; await options.onInteraction({ kind: 'question', interactionId: 'submit-retry-question', rpcId: 'submit-retry-question', sessionId, payload: { type: 'question/requested', sessionId, questions: [{ id: 'answer', question: '请回答后再继续' }], }, respond: async (result) => { submittedAnswers.push(result.value.answer.answers[0].custom); submitAttempts += 1; if (submitAttempts === 1) { firstSubmitStarted.resolve(); await releaseFirstSubmit.promise; throw new Error('temporary response failure'); } answered.resolve(); return { accepted: true }; }, }); await answered.promise; return '第一轮完成'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_user']), logger: { info() {}, warn() {}, error() {} }, }); const first = bridge.accept(event('submit-retry-start', '启动可重试交互')); await eventually(() => sent.some(({ text }) => text.includes('请回答后再继续'))); const firstAnswer = bridge.accept(event('submit-retry-answer', '第一次答案')); await firstSubmitStarted.promise; let nextSettled = false; const next = bridge.accept(event('submit-retry-next', '排队的下一个问题')) .finally(() => { nextSettled = true; }); releaseFirstSubmit.resolve(); await firstAnswer; await eventually(() => sent.some(({ text }) => text.includes('回答提交失败'))); assert.equal(nextSettled, false); assert.deepEqual(asked, ['启动可重试交互']); const retry = bridge.accept(event('submit-retry-again', '重试后的答案')); await Promise.all([retry, first, next]); assert.deepEqual(submittedAnswers, ['第一次答案', '重试后的答案']); assert.deepEqual(asked, ['启动可重试交互', '排队的下一个问题']); assert.deepEqual(sent.slice(-2).map(({ text }) => text), ['第一轮完成', '第二轮完成']); }); test('a non-text pending reply does not block the valid answer behind it', async () => { const fixture = stateFixture([['p2p:ou_user', 'session-invalid-reply']]); const sent = []; const invalidNoticeStarted = deferred(); const releaseInvalidNotice = deferred(); const answered = deferred(); let submitted; const bridge = new FeishuHarnessBridge({ client: textClient(async (message) => { if (message.text === '请用文字回答当前问题。') { invalidNoticeStarted.resolve(); await releaseInvalidNotice.promise; } sent.push(message); }), harness: { sessionExists: async () => true, createSession: async () => assert.fail('the existing session should be reused'), ask: async (sessionId, _text, options) => { await options.onInteraction({ kind: 'question', interactionId: 'invalid-reply-question', rpcId: 'invalid-reply-question', sessionId, payload: { type: 'question/requested', sessionId, questions: [{ id: 'answer', question: '请给出有效文字答案' }], }, respond: async (result) => { submitted = result; answered.resolve(); return { accepted: true }; }, }); await answered.promise; return '有效答案已收到'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_user']), }); const first = bridge.accept(event('invalid-reply-start', '启动交互')); await eventually(() => sent.some(({ text }) => text.includes('请给出有效文字答案'))); const invalid = bridge.accept(event('invalid-reply-image', '', { message_type: 'image', content: JSON.stringify({ image_key: 'img-test' }), })); await invalidNoticeStarted.promise; const valid = bridge.accept(event('invalid-reply-valid', '真正的答案')); releaseInvalidNotice.resolve(); await Promise.all([invalid, valid, first]); assert.deepEqual(submitted.value.answer.answers, [{ id: 'answer', selected: [], custom: '真正的答案', }]); assert.equal(sent.at(-1).text, '有效答案已收到'); }); test('an answer resolved elsewhere is not reinterpreted as a later prompt', async () => { const fixture = stateFixture([['p2p:ou_user', 'session-resolved-race']]); const originalMarkSeen = fixture.state.markSeen; const answerMarkStarted = deferred(); const releaseAnswerMark = deferred(); fixture.state.markSeen = async (id) => { if (id === 'resolved-answer-first') { answerMarkStarted.resolve(); await releaseAnswerMark.promise; } await originalMarkSeen(id); }; const sent = []; const asked = []; const resolved = deferred(); let resolveInteraction; const bridge = new FeishuHarnessBridge({ client: textClient(async (message) => sent.push(message)), harness: { sessionExists: async () => true, createSession: async () => assert.fail('the existing session should be reused'), ask: async (sessionId, text, options) => { asked.push(text); if (text === '后来的普通问题') return '后来问题的回答'; await options.onInteraction({ kind: 'question', interactionId: 'resolved-race-question', rpcId: 'resolved-race-question', sessionId, payload: { type: 'question/requested', sessionId, questions: [{ id: 'answer', question: '可能在其他客户端回答' }], }, respond: async () => ({ accepted: true }), }); resolveInteraction = async () => { await options.onInteractionResolved({ kind: 'question', interactionId: 'resolved-race-question', sessionId, outcome: 'answered', }); resolved.resolve(); }; await resolved.promise; return '第一轮已由其他客户端完成'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_user']), }); const first = bridge.accept(event('resolved-race-start', '启动外部解决竞态')); await eventually(() => typeof resolveInteraction === 'function'); const answer = bridge.accept(event('resolved-answer-first', '原本的问题答案')); await answerMarkStarted.promise; const later = bridge.accept(event('resolved-later-second', '后来的普通问题')); await resolveInteraction(); releaseAnswerMark.resolve(); await Promise.all([answer, first, later]); assert.deepEqual(asked, ['启动外部解决竞态', '后来的普通问题']); assert.equal(asked.includes('原本的问题答案'), false); assert.equal(sent.some(({ text }) => text.includes('已在其他客户端处理')), true); }); test('a late reply to a resolved Feishu question thread is discarded', async () => { const fixture = stateFixture([['p2p:ou_user', 'session-resolved-thread']]); const sent = []; const asked = []; const resolved = deferred(); let resolveInteraction; const bridge = new FeishuHarnessBridge({ client: textClient(async (message) => sent.push(message)), harness: { sessionExists: async () => true, createSession: async () => assert.fail('the existing session should be reused'), ask: async (sessionId, text, options) => { asked.push(text); await options.onInteraction({ kind: 'question', interactionId: 'resolved-thread-question', rpcId: 'resolved-thread-question', sessionId, payload: { type: 'question/requested', sessionId, questions: [{ id: 'late', question: '稍后会在其他客户端回答' }], }, respond: async () => ({ accepted: true }), }); resolveInteraction = async () => { await options.onInteractionResolved({ kind: 'question', interactionId: 'resolved-thread-question', sessionId, outcome: 'answered', }); resolved.resolve(); }; await resolved.promise; return '已由其他客户端完成'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_user']), }); const first = bridge.accept(event('resolved-thread-start', '启动线程迟到测试')); await eventually(() => typeof resolveInteraction === 'function' && sent.length === 1); await resolveInteraction(); await first; await bridge.accept(event('resolved-thread-late', '1', { root_id: 'resolved-thread-start', parent_id: 'om_test_1', thread_id: 'omt_resolved_thread', })); assert.deepEqual(asked, ['启动线程迟到测试']); assert.equal(sent.some(({ text }) => text.includes('已在其他客户端处理')), true); }); test('a question resolved while its next message is in flight tombstones that late thread', async () => { const fixture = stateFixture([['p2p:ou_user', 'session-resolved-inflight']]); const sent = []; const asked = []; const q2SendStarted = deferred(); const releaseQ2Send = deferred(); const resolved = deferred(); let resolveInteraction; let nextMessageSequence = 0; let q2MessageId; const client = { im: { v1: { message: { create: async (request) => { const messageId = `om_inflight_${++nextMessageSequence}`; const outgoing = { chatId: request.data.receive_id, text: JSON.parse(request.data.content).text, messageId, }; sent.push(outgoing); if (outgoing.text.includes('在途的第二问')) { q2MessageId = messageId; q2SendStarted.resolve(); await releaseQ2Send.promise; } return { code: 0, data: { message_id: messageId } }; } } } }, }; const bridge = new FeishuHarnessBridge({ client, harness: { sessionExists: async () => true, createSession: async () => assert.fail('the existing session should be reused'), ask: async (sessionId, text, options) => { asked.push(text); if (text !== '启动在途解决竞态') return '迟到回答被错误地当成普通 prompt'; await options.onInteraction({ kind: 'question', interactionId: 'resolved-inflight-question', rpcId: 'resolved-inflight-question', sessionId, payload: { type: 'question/requested', sessionId, questions: [ { id: 'first', question: '先回答第一问' }, { id: 'second', question: '在途的第二问' }, ], }, respond: async () => ({ accepted: true }), }); resolveInteraction = async () => { await options.onInteractionResolved({ kind: 'question', interactionId: 'resolved-inflight-question', sessionId, outcome: 'answered', }); resolved.resolve(); }; await resolved.promise; return '已在其他客户端完成'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_user']), }); const first = bridge.accept(event('resolved-inflight-start', '启动在途解决竞态')); await eventually(() => typeof resolveInteraction === 'function'); const firstAnswer = bridge.accept(event('resolved-inflight-first-answer', '第一问答案')); await q2SendStarted.promise; await resolveInteraction(); releaseQ2Send.resolve(); await Promise.all([firstAnswer, first]); await bridge.accept(event('resolved-inflight-late-q2-answer', '第二问的迟到答案', { root_id: 'resolved-inflight-start', parent_id: q2MessageId, thread_id: 'omt_resolved_inflight_q2', })); assert.deepEqual(asked, ['启动在途解决竞态']); assert.equal(sent.some(({ text }) => text.includes('已在其他客户端处理')), true); }); test('a q2 thread reply accepted before an in-flight send resolves is discarded after resolution', async () => { const fixture = stateFixture([['p2p:ou_user', 'session-resolved-accepted-inflight']]); const sent = []; const asked = []; const q2Delivered = deferred(); const releaseQ2Send = deferred(); const resolved = deferred(); let resolveInteraction; let nextMessageSequence = 0; let q2MessageId; const client = { im: { v1: { message: { create: async (request) => { const messageId = `om_accepted_inflight_${++nextMessageSequence}`; const outgoing = { chatId: request.data.receive_id, text: JSON.parse(request.data.content).text, messageId, }; sent.push(outgoing); if (outgoing.text.includes('已投递但 Promise 未返回的第二问')) { q2MessageId = messageId; q2Delivered.resolve(); await releaseQ2Send.promise; } return { code: 0, data: { message_id: messageId } }; } } } }, }; const bridge = new FeishuHarnessBridge({ client, harness: { sessionExists: async () => true, createSession: async () => assert.fail('the existing session should be reused'), ask: async (sessionId, text, options) => { asked.push(text); if (text !== '启动已接收回复竞态') return '已接收的迟到回复被错误地当成普通 prompt'; await options.onInteraction({ kind: 'question', interactionId: 'resolved-accepted-inflight-question', rpcId: 'resolved-accepted-inflight-question', sessionId, payload: { type: 'question/requested', sessionId, questions: [ { id: 'first', question: '先完成第一问' }, { id: 'second', question: '已投递但 Promise 未返回的第二问' }, ], }, respond: async () => ({ accepted: true }), }); resolveInteraction = async () => { await options.onInteractionResolved({ kind: 'question', interactionId: 'resolved-accepted-inflight-question', sessionId, outcome: 'answered', }); resolved.resolve(); }; await resolved.promise; return '已在其他客户端完成'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_user']), }); const first = bridge.accept(event('resolved-accepted-inflight-start', '启动已接收回复竞态')); await eventually(() => typeof resolveInteraction === 'function'); const firstAnswer = bridge.accept(event( 'resolved-accepted-inflight-first-answer', '第一问答案', )); await q2Delivered.promise; // Feishu has delivered q2 and can emit its thread reply, while the SDK // message.create Promise observed by the bridge is still pending. const alreadyAcceptedReply = bridge.accept(event( 'resolved-accepted-inflight-q2-answer', '第二问的在途答案', { root_id: 'resolved-accepted-inflight-start', parent_id: q2MessageId, thread_id: 'omt_resolved_accepted_inflight_q2', }, )); await resolveInteraction(); releaseQ2Send.resolve(); await Promise.all([alreadyAcceptedReply, firstAnswer, first]); assert.deepEqual(asked, ['启动已接收回复竞态']); assert.equal(sent.some(({ text }) => text.includes('已在其他客户端处理')), true); }); test('a recovered orphan question is cancelled without exposing its old content', async () => { const fixture = stateFixture([['p2p:ou_user', 'session-orphan-recovery']]); const sent = []; let recoveredResponse; const bridge = new FeishuHarnessBridge({ client: textClient(async (message) => sent.push(message)), harness: { sessionExists: async () => true, createSession: async () => assert.fail('the existing session should be reused'), ask: async (sessionId, _text, options) => { await options.onInteraction({ kind: 'question', interactionId: 'orphan-secret-question', rpcId: 'orphan-secret-question', sessionId, recovered: true, payload: { type: 'question/requested', sessionId, questions: [{ id: 'secret', question: '旧会话中的敏感问题内容' }], }, respond: async (result) => { recoveredResponse = result; return { accepted: true }; }, }); return '新的消息已继续'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_user']), }); await bridge.accept(event('orphan-recovery', '新的会话消息')); assert.deepEqual(recoveredResponse, { ok: false, error: { code: 'cancelled', message: 'Feishu safely cancelled an interaction left by an earlier client.', details: {}, }, }); assert.equal(sent.some(({ text }) => text.includes('旧会话中的敏感问题内容')), false); assert.equal(sent.some(({ text }) => text.includes('遗留的待回答问题')), true); assert.equal(sent.at(-1).text, '新的消息已继续'); }); test('a multi-question interaction keeps ordered canonical answers', async () => { const fixture = stateFixture([['p2p:ou_user', 'session-question-batch']]); const sent = []; const response = deferred(); const bridge = new FeishuHarnessBridge({ client: textClient(async (message) => sent.push(message)), harness: { sessionExists: async () => true, createSession: async () => assert.fail('the existing session should be reused'), ask: async (sessionId, _text, options) => { await options.onInteraction({ kind: 'question', interactionId: 'batch-question', rpcId: 'batch-question', sessionId, payload: { type: 'question/requested', sessionId, questions: [ { id: 'language', question: '选择回答语言', options: [{ label: '中文' }, { label: 'English' }], }, { id: 'deliverables', question: '选择交付内容', multiSelect: true, options: [{ label: '测试' }, { label: '文档' }], }, ], }, respond: async (result) => { response.resolve(result); return { accepted: true }; }, }); await response.promise; return '批量问题已完成'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_user']), }); const first = bridge.accept(event('batch-start', '请分步提问')); await eventually(() => sent.some(({ text }) => ( text.includes('(1/2)') && text.includes('选择回答语言') ))); await bridge.accept(event('batch-language', '2')); await eventually(() => sent.some(({ text }) => ( text.includes('(2/2)') && text.includes('选择交付内容') ))); await bridge.accept(event('batch-deliverables', '1,文档,发布说明')); assert.deepEqual(await response.promise, { ok: true, value: { sessionId: 'session-question-batch', answer: { answers: [ { id: 'language', selected: ['English'] }, { id: 'deliverables', selected: ['测试', '文档'], custom: '发布说明' }, ], }, }, }); await first; assert.equal(sent.at(-1).text, '批量问题已完成'); }); test('the second answer bypasses the first answer reaction-finalization window', async () => { const fixture = stateFixture([['p2p:ou_user', 'session-multi-window']]); const sent = []; const asked = []; const firstAnswerDoneStarted = deferred(); const releaseFirstAnswerDone = deferred(); const submitted = deferred(); const releaseTurn = deferred(); const bridge = new FeishuHarnessBridge({ client: textClient(async (message) => sent.push(message)), channel: { addReaction: async (messageId, emojiType) => { if (messageId === 'multi-window-first-answer' && emojiType === 'DONE') { firstAnswerDoneStarted.resolve(); await releaseFirstAnswerDone.promise; } return `reaction-${messageId}-${emojiType}`; }, removeReaction: async () => undefined, }, harness: { sessionExists: async () => true, createSession: async () => assert.fail('the existing session should be reused'), ask: async (sessionId, text, options) => { asked.push(text); if (text !== '启动多问题窗口') return '第二问答案被错误地当成普通 prompt'; await options.onInteraction({ kind: 'question', interactionId: 'multi-window-question', rpcId: 'multi-window-question', sessionId, payload: { type: 'question/requested', sessionId, questions: [ { id: 'first', question: '第一问', options: [{ label: '甲' }, { label: '乙' }], }, { id: 'second', question: '第二问', options: [{ label: '丙' }, { label: '丁' }], }, ], }, respond: async (result) => { submitted.resolve(result); releaseTurn.resolve(); return { accepted: true }; }, }); await releaseTurn.promise; return '两个问题均已完成'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_user']), }); const first = bridge.accept(event('multi-window-start', '启动多问题窗口')); await eventually(() => sent.some(({ text }) => text.includes('第一问'))); const firstAnswer = bridge.accept(event('multi-window-first-answer', '1')); await eventually(() => sent.some(({ text }) => text.includes('第二问'))); await firstAnswerDoneStarted.promise; const secondAnswer = bridge.accept(event('multi-window-second-answer', '2')); let submittedResult; let deadline; try { submittedResult = await Promise.race([ submitted.promise, new Promise((_, reject) => { deadline = setTimeout( () => reject(new Error( 'the second answer deadlocked behind the first answer DONE reaction', )), 500, ); }), ]); } finally { clearTimeout(deadline); // Keep the red test from leaving unresolved work behind in the test process. releaseFirstAnswerDone.resolve(); releaseTurn.resolve(); await Promise.allSettled([firstAnswer, secondAnswer, first]); } assert.deepEqual(submittedResult, { ok: true, value: { sessionId: 'session-multi-window', answer: { answers: [ { id: 'first', selected: ['甲'] }, { id: 'second', selected: ['丁'] }, ], }, }, }); assert.deepEqual(asked, ['启动多问题窗口']); assert.equal(sent.at(-1).text, '两个问题均已完成'); }); test('a group interaction question tells the user to mention the bot again', async () => { const fixture = stateFixture([['group:oc_group_mention', 'session-group-mention']]); const sent = []; const bridge = new FeishuHarnessBridge({ client: textClient(async (message) => sent.push(message)), harness: { sessionExists: async () => true, createSession: async () => assert.fail('the group session should already exist'), ask: async (sessionId, _text, options) => { await options.onInteraction({ kind: 'question', interactionId: 'group-mention-question', rpcId: 'group-mention-question', sessionId, payload: { type: 'question/requested', sessionId, questions: [{ id: 'environment', question: '请选择群聊测试环境', options: [{ label: '测试环境' }, { label: '生产环境' }], }], }, respond: async () => ({ accepted: true }), }); return '群聊提示测试结束'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_a']), }); await bridge.accept(event('group-mention-start', '@机器人 请先提问', { senderOpenId: 'ou_a', chat_type: 'group', chat_id: 'oc_group_mention', mentions: [{ key: '@机器人' }], })); const questionText = sent.find(({ text }) => text.includes('请选择群聊测试环境'))?.text; assert.match(questionText ?? '', /群聊中请\s*@机器人\s*后发送答案/); }); test('only the actor who started a group interaction can answer it', async () => { const fixture = stateFixture([['group:oc_group_actor', 'session-group-actor']]); const asked = []; const submitted = deferred(); let interactionSubmitted = false; const bridge = new FeishuHarnessBridge({ client: textClient(async () => undefined), harness: { sessionExists: async () => true, createSession: async () => assert.fail('the group session should already exist'), ask: async (sessionId, text, options) => { asked.push(text); if (text !== '甲发起交互') return '普通群消息已处理'; await options.onInteraction({ kind: 'question', interactionId: 'group-actor-question', rpcId: 'group-actor-question', sessionId, payload: { type: 'question/requested', sessionId, questions: [{ id: 'actor', question: '只能由甲回答' }], }, respond: async (result) => { interactionSubmitted = true; submitted.resolve(result); return { accepted: true }; }, }); await submitted.promise; return '甲的交互已完成'; }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_a', 'ou_b']), }); const first = bridge.accept(event('group-actor-start', '甲发起交互', { senderOpenId: 'ou_a', chat_type: 'group', chat_id: 'oc_group_actor', })); await eventually(() => asked.length === 1); const intruder = bridge.accept(event('group-actor-b', '乙试图代答', { senderOpenId: 'ou_b', chat_type: 'group', chat_id: 'oc_group_actor', })); await new Promise((resolve) => setTimeout(resolve, 20)); assert.equal(interactionSubmitted, false); assert.deepEqual(asked, ['甲发起交互']); await bridge.accept(event('group-actor-a', '甲的答案', { senderOpenId: 'ou_a', chat_type: 'group', chat_id: 'oc_group_actor', })); assert.deepEqual((await submitted.promise).value.answer.answers, [{ id: 'actor', selected: [], custom: '甲的答案', }]); await Promise.all([first, intruder]); assert.deepEqual(asked, ['甲发起交互', '乙试图代答']); }); test('aborting an active Feishu turn removes its processing reaction', async () => { const fixture = stateFixture([['p2p:ou_user', 'session-abort-reaction']]); const controller = new AbortController(); const reactions = []; const removed = []; const askStarted = deferred(); const bridge = new FeishuHarnessBridge({ client: textClient(async () => undefined), channel: { addReaction: async (messageId, emojiType) => { reactions.push({ messageId, emojiType }); return `reaction-${emojiType}`; }, removeReaction: async (messageId, reactionId) => removed.push({ messageId, reactionId }), stream: async (_chatId, input) => input.markdown({ setContent: async () => undefined }), }, harness: { sessionExists: async () => true, createSession: async () => assert.fail('the existing session should be reused'), ask: async (_sessionId, _text, options) => { askStarted.resolve(); await new Promise((resolve, reject) => { if (options.signal.aborted) { reject(options.signal.reason); return; } options.signal.addEventListener('abort', () => reject(options.signal.reason), { once: true }); }); }, }, state: fixture.state, status: bridgeStatus(), allowedSenderOpenIds: new Set(['ou_user']), signal: controller.signal, }); const processing = bridge.accept(event('abort-reaction', '启动后停止')); await askStarted.promise; controller.abort(new DOMException('runtime stopped', 'AbortError')); await processing; assert.deepEqual(reactions, [{ messageId: 'abort-reaction', emojiType: 'OnIt' }]); assert.deepEqual(removed, [{ messageId: 'abort-reaction', reactionId: 'reaction-OnIt', }]); }); 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'); });