185 lines
5.4 KiB
TypeScript
185 lines
5.4 KiB
TypeScript
// @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();
|
|
});
|
|
});
|