Fix(queue): auto-send queued messages FIFO (#1254)
* fix(queue): auto-send queued messages FIFO * test(queue): cover FIFO queued auto-send payload * test(queue): stub config store in auto-send tests * fix(test): complete Agent type in queued auto-send test --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
2c25bfc8d7
commit
462cebdf95
@@ -0,0 +1,104 @@
|
|||||||
|
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
||||||
|
import type { Agent } from '@opencode-ai/sdk/v2';
|
||||||
|
import type { QueuedMessage } from '../stores/messageQueueStore';
|
||||||
|
|
||||||
|
let visibleAgents: Agent[] = [];
|
||||||
|
|
||||||
|
const getVisibleAgentsMock = mock(() => visibleAgents);
|
||||||
|
|
||||||
|
mock.module('@/stores/useConfigStore', () => ({
|
||||||
|
useConfigStore: {
|
||||||
|
getState: () => ({
|
||||||
|
getVisibleAgents: getVisibleAgentsMock,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { buildQueuedAutoSendPayload } from './useQueuedMessageAutoSend';
|
||||||
|
|
||||||
|
describe('buildQueuedAutoSendPayload', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
visibleAgents = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns only the first queued message for auto-send', () => {
|
||||||
|
const queue: QueuedMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'queued-1',
|
||||||
|
content: 'first queued message',
|
||||||
|
createdAt: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'queued-2',
|
||||||
|
content: 'second queued message',
|
||||||
|
createdAt: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const payload = buildQueuedAutoSendPayload(queue);
|
||||||
|
|
||||||
|
expect(payload).not.toBeNull();
|
||||||
|
expect(payload?.queuedMessageId).toBe('queued-1');
|
||||||
|
expect(payload?.primaryText).toBe('first queued message');
|
||||||
|
expect(payload?.primaryAttachments).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses the configured visible agents when parsing queued mentions', () => {
|
||||||
|
visibleAgents = [
|
||||||
|
{
|
||||||
|
name: 'Builder',
|
||||||
|
mode: 'subagent',
|
||||||
|
permission: [],
|
||||||
|
options: {},
|
||||||
|
} as Agent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const queue: QueuedMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'queued-mention',
|
||||||
|
content: '@Builder please take this',
|
||||||
|
createdAt: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const payload = buildQueuedAutoSendPayload(queue);
|
||||||
|
|
||||||
|
expect(payload).not.toBeNull();
|
||||||
|
expect(payload?.agentMentionName).toBe('Builder');
|
||||||
|
expect(payload?.primaryText).toBe('@Builder please take this');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves attachment-only queued messages as sendable payloads', () => {
|
||||||
|
const queue: QueuedMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'queued-attachments',
|
||||||
|
content: '',
|
||||||
|
createdAt: 1,
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
id: 'file-1',
|
||||||
|
filename: 'notes.txt',
|
||||||
|
mimeType: 'text/plain',
|
||||||
|
size: 5,
|
||||||
|
source: 'local',
|
||||||
|
file: new File(['hello'], 'notes.txt', { type: 'text/plain' }),
|
||||||
|
dataUrl: 'data:text/plain;base64,aGVsbG8=',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'queued-2',
|
||||||
|
content: 'later queued message',
|
||||||
|
createdAt: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const payload = buildQueuedAutoSendPayload(queue);
|
||||||
|
|
||||||
|
expect(payload).not.toBeNull();
|
||||||
|
expect(payload?.queuedMessageId).toBe('queued-attachments');
|
||||||
|
expect(payload?.primaryText).toBe('');
|
||||||
|
expect(payload?.primaryAttachments).toHaveLength(1);
|
||||||
|
expect(payload?.primaryAttachments[0]?.filename).toBe('notes.txt');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { AttachedFile } from '@/stores/types/sessionTypes';
|
|
||||||
import { useMessageQueueStore, type QueuedMessage } from '@/stores/messageQueueStore';
|
import { useMessageQueueStore, type QueuedMessage } from '@/stores/messageQueueStore';
|
||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
import { useSelectionStore } from '@/sync/selection-store';
|
import { useSelectionStore } from '@/sync/selection-store';
|
||||||
@@ -21,37 +20,21 @@ const hasRecentAbort = (sessionId: string): boolean => {
|
|||||||
return Date.now() - abortRecord.timestamp < RECENT_ABORT_WINDOW_MS;
|
return Date.now() - abortRecord.timestamp < RECENT_ABORT_WINDOW_MS;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildQueuedPayload = (queue: QueuedMessage[]) => {
|
export const buildQueuedAutoSendPayload = (queue: QueuedMessage[]) => {
|
||||||
const agents = useConfigStore.getState().getVisibleAgents();
|
const queued = queue[0];
|
||||||
let primaryText = '';
|
if (!queued) {
|
||||||
let primaryAttachments: AttachedFile[] = [];
|
return null;
|
||||||
let agentMentionName: string | undefined;
|
|
||||||
const additionalParts: Array<{ text: string; attachments?: AttachedFile[] }> = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < queue.length; i += 1) {
|
|
||||||
const queued = queue[i];
|
|
||||||
const { sanitizedText, mention } = parseAgentMentions(queued.content, agents);
|
|
||||||
|
|
||||||
if (!agentMentionName && mention?.name) {
|
|
||||||
agentMentionName = mention.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i === 0) {
|
|
||||||
primaryText = sanitizedText;
|
|
||||||
primaryAttachments = queued.attachments ?? [];
|
|
||||||
} else {
|
|
||||||
additionalParts.push({
|
|
||||||
text: sanitizedText,
|
|
||||||
attachments: queued.attachments,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const agents = useConfigStore.getState().getVisibleAgents();
|
||||||
|
const { sanitizedText, mention } = parseAgentMentions(queued.content, agents);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
primaryText,
|
queuedMessageId: queued.id,
|
||||||
primaryAttachments,
|
primaryText: sanitizedText,
|
||||||
agentMentionName,
|
primaryAttachments: queued.attachments ?? [],
|
||||||
additionalParts: additionalParts.length > 0 ? additionalParts : undefined,
|
agentMentionName: mention?.name,
|
||||||
|
sendConfig: queued.sendConfig,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,13 +108,16 @@ export function useQueuedMessageAutoSend(enabledOrOptions?: boolean | { enabled?
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = buildQueuedPayload(queueSnapshot);
|
const payload = buildQueuedAutoSendPayload(queueSnapshot);
|
||||||
if (!payload.primaryText && payload.primaryAttachments.length === 0 && !payload.additionalParts?.length) {
|
if (!payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!payload.primaryText && payload.primaryAttachments.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use send config captured at queue time; fall back to current config
|
// Use send config captured at queue time; fall back to current config
|
||||||
const captured = queueSnapshot[0]?.sendConfig;
|
const captured = payload.sendConfig;
|
||||||
const resolved = captured?.providerID && captured?.modelID
|
const resolved = captured?.providerID && captured?.modelID
|
||||||
? captured
|
? captured
|
||||||
: resolveSessionSendConfig(sessionId);
|
: resolveSessionSendConfig(sessionId);
|
||||||
@@ -149,15 +135,13 @@ export function useQueuedMessageAutoSend(enabledOrOptions?: boolean | { enabled?
|
|||||||
resolved.agent,
|
resolved.agent,
|
||||||
payload.primaryAttachments,
|
payload.primaryAttachments,
|
||||||
payload.agentMentionName,
|
payload.agentMentionName,
|
||||||
payload.additionalParts,
|
undefined,
|
||||||
resolved.variant,
|
resolved.variant,
|
||||||
'normal'
|
'normal'
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeFromQueue = useMessageQueueStore.getState().removeFromQueue;
|
const removeFromQueue = useMessageQueueStore.getState().removeFromQueue;
|
||||||
queueSnapshot.forEach((item) => {
|
removeFromQueue(sessionId, payload.queuedMessageId);
|
||||||
removeFromQueue(sessionId, item.id);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[queue] queued auto-send failed:', error);
|
console.warn('[queue] queued auto-send failed:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user