fix: preserve error reactions for failed interactions

This commit is contained in:
xmanrui 2026-08-25 17:15:06 +08:00
parent da01e3acee
commit 1445b93891
3 changed files with 31 additions and 5 deletions

File diff suppressed because one or more lines are too long

View file

@ -866,6 +866,7 @@ export class TextHarnessBridge {
try { try {
await this.#presentInteraction(pending); await this.#presentInteraction(pending);
} catch (error) { } catch (error) {
message.statusReaction?.error();
this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label }); this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
this.#logger.error?.( this.#logger.error?.(
`[dsh-im:${this.#descriptor.key}] failed to retry an interaction question:`, `[dsh-im:${this.#descriptor.key}] failed to retry an interaction question:`,
@ -904,6 +905,7 @@ export class TextHarnessBridge {
try { try {
await this.#presentInteraction(pending); await this.#presentInteraction(pending);
} catch (error) { } catch (error) {
message.statusReaction?.error();
this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label }); this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
this.#logger.error?.( this.#logger.error?.(
`[dsh-im:${this.#descriptor.key}] failed to send the next interaction question:`, `[dsh-im:${this.#descriptor.key}] failed to send the next interaction question:`,
@ -946,7 +948,11 @@ export class TextHarnessBridge {
message.statusReaction?.clear(); message.statusReaction?.clear();
return; return;
} }
if (this.#pendingInteractions.get(key) !== pending) return; if (this.#pendingInteractions.get(key) !== pending) {
message.statusReaction?.clear();
return;
}
message.statusReaction?.error();
pending.submitting = false; pending.submitting = false;
pending.answers.pop(); pending.answers.pop();
pending.index -= 1; pending.index -= 1;

View file

@ -141,9 +141,10 @@ function createBridge({
bot, bot,
signal, signal,
logger, logger,
reactions,
} = {}) { } = {}) {
return new TextHarnessBridge({ return new TextHarnessBridge({
descriptor: { key: 'test', label: 'Test' }, descriptor: { key: 'test', label: 'Test', reactions },
bot, bot,
harness, harness,
state, state,
@ -2264,11 +2265,22 @@ test('deduplicates replays and safely closes recovered questions and approvals',
test('keeps a failed interaction response pending so the actor can retry', async () => { test('keeps a failed interaction response pending so the actor can retry', async () => {
const fixture = stateFixture(); const fixture = stateFixture();
const sent = []; const sent = [];
const reactions = [];
const completed = deferred(); const completed = deferred();
const submittedAnswers = []; const submittedAnswers = [];
const bridge = createBridge({ const bridge = createBridge({
state: fixture.state, state: fixture.state,
bot: { sendText: async (target, text) => sent.push({ target, text }) }, reactions: { processing: 'eyes', success: 'done', error: 'error' },
bot: {
addReaction: async (target, emoji) => {
reactions.push(['add', target.id, emoji]);
return emoji;
},
removeReaction: async (target, emoji) => {
reactions.push(['remove', target.id, emoji]);
},
sendText: async (target, text) => sent.push({ target, text }),
},
harness: { harness: {
createSession: async () => 'session-one', createSession: async () => 'session-one',
ask: async (sessionId, _text, options) => { ask: async (sessionId, _text, options) => {
@ -2290,13 +2302,21 @@ test('keeps a failed interaction response pending so the actor can retry', async
const processing = bridge.accept(message('retry-start', '启动可重试交互')); const processing = bridge.accept(message('retry-start', '启动可重试交互'));
await eventually(() => sent.some(({ text }) => text.includes('请回答'))); await eventually(() => sent.some(({ text }) => text.includes('请回答')));
await bridge.accept(message('retry-first', '第一次答案')); await bridge.accept(message('retry-first', '第一次答案', {
reactionTarget: { id: 'source-retry-first' },
}));
await eventually(() => reactions.length === 3);
assert.equal(sent.some(({ text }) => text.includes('回答提交失败')), true); assert.equal(sent.some(({ text }) => text.includes('回答提交失败')), true);
await bridge.accept(message('retry-second', '重试后的答案')); await bridge.accept(message('retry-second', '重试后的答案'));
await processing; await processing;
assert.deepEqual(submittedAnswers, ['第一次答案', '重试后的答案']); assert.deepEqual(submittedAnswers, ['第一次答案', '重试后的答案']);
assert.equal(sent.at(-1).text, '重试成功'); assert.equal(sent.at(-1).text, '重试成功');
assert.deepEqual(reactions, [
['add', 'source-retry-first', 'eyes'],
['remove', 'source-retry-first', 'eyes'],
['add', 'source-retry-first', 'error'],
]);
}); });
test('notifies the actor when an in-flight response resolves elsewhere before rejection', async () => { test('notifies the actor when an in-flight response resolves elsewhere before rejection', async () => {