feat: 完善智能体图片聊天与会话恢复
- 增加私有图片上传、绑定、历史回显与生命周期清理 - 支持输入草稿恢复、图片交互和模型图片能力约束 - 修复旧脏会话幂等删除与前端会话恢复
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useUserStore } from '@easyflow/stores';
|
||||
|
||||
import {
|
||||
allocateAgentComposerSession,
|
||||
deleteAgentComposerDraft,
|
||||
getAgentComposerDraft,
|
||||
saveAgentComposerDraft,
|
||||
} from './mediaApi';
|
||||
import { useAgentComposerDraft } from './useAgentComposerDraft';
|
||||
|
||||
vi.mock('./mediaApi', () => ({
|
||||
allocateAgentComposerSession: vi.fn(),
|
||||
deleteAgentChatImage: vi.fn(),
|
||||
deleteAgentComposerDraft: vi.fn(),
|
||||
getAgentComposerDraft: vi.fn(),
|
||||
loadAgentChatImage: vi.fn(),
|
||||
saveAgentComposerDraft: vi.fn(),
|
||||
uploadAgentChatImage: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('useAgentComposerDraft', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
useUserStore().setUserInfo({
|
||||
avatar: '',
|
||||
id: 'user-1',
|
||||
loginName: 'admin',
|
||||
nickname: '管理员',
|
||||
tenantId: 'tenant-1',
|
||||
});
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(allocateAgentComposerSession).mockResolvedValue({
|
||||
data: { mode: 'FORMAL', sessionId: '100' },
|
||||
errorCode: 0,
|
||||
});
|
||||
vi.mocked(getAgentComposerDraft).mockResolvedValue({
|
||||
data: null,
|
||||
errorCode: 0,
|
||||
});
|
||||
vi.mocked(deleteAgentComposerDraft).mockResolvedValue({
|
||||
data: undefined,
|
||||
errorCode: 0,
|
||||
});
|
||||
vi.mocked(saveAgentComposerDraft).mockImplementation(async (draft) => ({
|
||||
data: {
|
||||
...draft,
|
||||
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
revision: draft.revision + 1,
|
||||
},
|
||||
errorCode: 0,
|
||||
}));
|
||||
});
|
||||
|
||||
it('restores an unsynced local shadow and writes it back to Redis', async () => {
|
||||
vi.useFakeTimers();
|
||||
const first = useAgentComposerDraft('FORMAL');
|
||||
await first.activate('agent-1');
|
||||
first.text.value = '刷新前尚未发送的内容';
|
||||
first.scheduleSave();
|
||||
|
||||
const restored = useAgentComposerDraft('FORMAL');
|
||||
await restored.activate('agent-1');
|
||||
|
||||
expect(restored.sessionId.value).toBe('100');
|
||||
expect(restored.text.value).toBe('刷新前尚未发送的内容');
|
||||
expect(saveAgentComposerDraft).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: 'agent-1',
|
||||
sessionId: '100',
|
||||
text: '刷新前尚未发送的内容',
|
||||
}),
|
||||
);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('keeps local shadows isolated by the logged-in account', async () => {
|
||||
vi.useFakeTimers();
|
||||
const first = useAgentComposerDraft('FORMAL');
|
||||
await first.activate('agent-1');
|
||||
first.text.value = '账号一的草稿';
|
||||
first.scheduleSave();
|
||||
|
||||
useUserStore().setUserInfo({
|
||||
avatar: '',
|
||||
id: 'user-2',
|
||||
loginName: 'other',
|
||||
nickname: '其他用户',
|
||||
tenantId: 'tenant-1',
|
||||
});
|
||||
vi.mocked(allocateAgentComposerSession).mockResolvedValueOnce({
|
||||
data: { mode: 'FORMAL', sessionId: '200' },
|
||||
errorCode: 0,
|
||||
});
|
||||
const second = useAgentComposerDraft('FORMAL');
|
||||
await second.activate('agent-1');
|
||||
|
||||
expect(second.sessionId.value).toBe('200');
|
||||
expect(second.text.value).toBe('');
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('serializes a debounced save and the send-time flush', async () => {
|
||||
vi.useFakeTimers();
|
||||
let resolveFirstSave: (() => void) | undefined;
|
||||
vi.mocked(saveAgentComposerDraft).mockImplementation((draft) => {
|
||||
if (!resolveFirstSave) {
|
||||
return new Promise((resolve) => {
|
||||
resolveFirstSave = () =>
|
||||
resolve({
|
||||
data: { ...draft, revision: 1 },
|
||||
errorCode: 0,
|
||||
});
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
data: { ...draft, revision: draft.revision + 1 },
|
||||
errorCode: 0,
|
||||
});
|
||||
});
|
||||
const composer = useAgentComposerDraft('FORMAL');
|
||||
await composer.activate('agent-1');
|
||||
composer.text.value = '并发保存内容';
|
||||
composer.scheduleSave();
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
await Promise.resolve();
|
||||
|
||||
const flushPromise = composer.flush();
|
||||
await Promise.resolve();
|
||||
expect(saveAgentComposerDraft).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolveFirstSave?.();
|
||||
await flushPromise;
|
||||
|
||||
expect(saveAgentComposerDraft).toHaveBeenCalledTimes(2);
|
||||
expect(saveAgentComposerDraft).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ revision: 1, text: '并发保存内容' }),
|
||||
);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('waits for an in-flight save before clearing an accepted draft', async () => {
|
||||
vi.useFakeTimers();
|
||||
let resolveSave: (() => void) | undefined;
|
||||
vi.mocked(saveAgentComposerDraft).mockImplementation(
|
||||
(draft) =>
|
||||
new Promise((resolve) => {
|
||||
resolveSave = () =>
|
||||
resolve({
|
||||
data: { ...draft, revision: 1 },
|
||||
errorCode: 0,
|
||||
});
|
||||
}),
|
||||
);
|
||||
const composer = useAgentComposerDraft('DRAFT');
|
||||
await composer.activate('agent-1', 'agent-draft-100');
|
||||
composer.text.value = '试运行内容';
|
||||
composer.scheduleSave();
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
await Promise.resolve();
|
||||
|
||||
const accepted = composer.markAccepted();
|
||||
await Promise.resolve();
|
||||
expect(deleteAgentComposerDraft).not.toHaveBeenCalled();
|
||||
|
||||
resolveSave?.();
|
||||
await accepted;
|
||||
|
||||
expect(deleteAgentComposerDraft).toHaveBeenCalledWith({
|
||||
agentId: 'agent-1',
|
||||
deleteUploads: false,
|
||||
imageUploadIds: [],
|
||||
mode: 'DRAFT',
|
||||
sessionId: 'agent-draft-100',
|
||||
});
|
||||
expect(composer.text.value).toBe('');
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user