fix(design): separate progress from chat replies
This commit is contained in:
@@ -74,6 +74,14 @@ class FakeEventSource {
|
||||
}
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((resolvePromise) => {
|
||||
resolve = resolvePromise;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
async function loadedStore(eventSource = new FakeEventSource()) {
|
||||
openEventsMock.mockResolvedValue(eventSource as unknown as EventSource);
|
||||
await useImageWorkspaceStore.getState().load();
|
||||
@@ -110,6 +118,87 @@ describe('V2 Living Form store', () => {
|
||||
expect(openEventsMock).toHaveBeenCalledWith('workspace-1', 'session-1', undefined);
|
||||
});
|
||||
|
||||
it('closes an obsolete event source when a replacement connection wins the race', async () => {
|
||||
const workspace = designWorkspaceFixture();
|
||||
useImageWorkspaceStore.setState({
|
||||
status: 'ready',
|
||||
activeWorkspaceId: workspace.workspace.workspaceId,
|
||||
workspace,
|
||||
});
|
||||
const firstOpen = deferred<EventSource>();
|
||||
const secondOpen = deferred<EventSource>();
|
||||
const firstSource = new FakeEventSource();
|
||||
const secondSource = new FakeEventSource();
|
||||
openEventsMock
|
||||
.mockReturnValueOnce(firstOpen.promise)
|
||||
.mockReturnValueOnce(secondOpen.promise);
|
||||
|
||||
useImageWorkspaceStore.getState().connectEvents();
|
||||
useImageWorkspaceStore.getState().disconnectEvents();
|
||||
useImageWorkspaceStore.getState().connectEvents();
|
||||
|
||||
firstOpen.resolve(firstSource as unknown as EventSource);
|
||||
await Promise.resolve();
|
||||
secondOpen.resolve(secondSource as unknown as EventSource);
|
||||
await vi.waitFor(() => expect(openEventsMock).toHaveBeenCalledTimes(2));
|
||||
|
||||
expect(firstSource.close).toHaveBeenCalledOnce();
|
||||
expect(secondSource.close).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows a new event connection after opening the previous one failed', async () => {
|
||||
const workspace = designWorkspaceFixture();
|
||||
const recoveredSource = new FakeEventSource();
|
||||
useImageWorkspaceStore.setState({
|
||||
status: 'ready',
|
||||
activeWorkspaceId: workspace.workspace.workspaceId,
|
||||
workspace,
|
||||
});
|
||||
openEventsMock
|
||||
.mockRejectedValueOnce(new Error('temporary event connection failure'))
|
||||
.mockResolvedValueOnce(recoveredSource as unknown as EventSource);
|
||||
|
||||
useImageWorkspaceStore.getState().connectEvents();
|
||||
await vi.waitFor(() => expect(useImageWorkspaceStore.getState().eventState).toBe('degraded'));
|
||||
useImageWorkspaceStore.getState().connectEvents();
|
||||
await vi.waitFor(() => expect(openEventsMock).toHaveBeenCalledTimes(2));
|
||||
|
||||
expect(recoveredSource.close).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses assistant chunk indexes to ignore replayed deltas', async () => {
|
||||
const source = await loadedStore();
|
||||
const baseEvent = {
|
||||
type: 'design.assistant.delta',
|
||||
workspaceId: 'workspace-1',
|
||||
directionId: 'direction-1',
|
||||
clientOperationId: 'operation-chat-1',
|
||||
directionRevision: 4,
|
||||
} as const;
|
||||
source.emit('design.assistant.delta', {
|
||||
...baseEvent,
|
||||
id: 'session-1:7',
|
||||
chunkIndex: 0,
|
||||
delta: '收到',
|
||||
});
|
||||
source.emit('design.assistant.delta', {
|
||||
...baseEvent,
|
||||
id: 'session-1:7-replayed',
|
||||
chunkIndex: 0,
|
||||
delta: '收到',
|
||||
});
|
||||
source.emit('design.assistant.delta', {
|
||||
...baseEvent,
|
||||
id: 'session-1:8',
|
||||
chunkIndex: 1,
|
||||
delta: ',正在整理',
|
||||
});
|
||||
|
||||
expect(useImageWorkspaceStore.getState().assistantStreams).toEqual({
|
||||
'operation-chat-1': '收到,正在整理',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps a field draft separate until the direct edit succeeds', async () => {
|
||||
await loadedStore();
|
||||
useImageWorkspaceStore.getState().setFieldDraft('output.aspect_ratio', '16:9');
|
||||
|
||||
Reference in New Issue
Block a user