fix(coding): preserve renderer recovery state

This commit is contained in:
2026-08-23 22:27:21 +08:00
parent 36626c88b5
commit 1abb2fff36
3 changed files with 125 additions and 7 deletions

View File

@@ -194,6 +194,46 @@ describe('coding Conversation store', () => {
expect(store.getState().entriesByConversationId['conversation-b'].reducer.invalidation).toBeNull();
});
it('replays target patches that arrive while a gap snapshot is loading', async () => {
const recovery = deferred<ConversationSnapshot>();
const getSnapshot = vi.fn(() => recovery.promise);
const store = createCodingConversationStore({
getSnapshot,
openEvents: vi.fn(),
submitPrompt: vi.fn(),
createId: ids(),
});
store.getState().applySnapshotEvent(snapshotEvent(snapshot('conversation-a')));
store.getState().applyPatchEvent(patchEvent('conversation-a', 2, {
op: 'run.state',
run: { status: 'running', runId: 'run-gap' },
}));
store.getState().applyPatchEvent(patchEvent('conversation-a', 3, {
op: 'run.state',
run: {
status: 'error',
runId: 'run-gap',
terminalReason: 'failed',
},
}));
recovery.resolve({
...snapshot('conversation-a', 1, 2),
run: { status: 'running', runId: 'run-gap' },
});
await waitFor(() => {
expect(selectCodingConversationSnapshot('conversation-a')(store.getState()))
.toMatchObject({
cursor: { workerGeneration: 1, seq: 3 },
run: { status: 'error', runId: 'run-gap', terminalReason: 'failed' },
});
});
expect(getSnapshot).toHaveBeenCalledTimes(1);
expect(store.getState().entriesByConversationId['conversation-a'].reducer.invalidation)
.toBeNull();
});
it('uses native EventSource reconnect without reopening or replaying a mutation', async () => {
const source = new FakeEventSource();
const openEvents = vi.fn(async () => source as unknown as EventSource);
@@ -276,6 +316,9 @@ describe('coding Conversation store', () => {
await submission;
expect(store.getState().requestsByConversationId['conversation-a']['request-1'].status)
.toBe('accepted');
store.getState().applySnapshotEvent(snapshotEvent(snapshot('conversation-a')));
expect(selectCodingConversationSnapshot('conversation-a')(store.getState())?.nodes[0])
.toMatchObject({ id: 'node-1', clientRequestId: 'request-1', status: 'optimistic' });
store.getState().applyPatchEvent(patchEvent('conversation-a', 1, {
op: 'message.upsert',
node: {
@@ -350,6 +393,7 @@ describe('coding Conversation store', () => {
})).rejects.toThrow('Delivery is uncertain');
source.fail();
source.open();
source.emit('snapshot', snapshotEvent(snapshot('conversation-a')));
expect(store.getState().draftsByConversationId['conversation-a'].text).toBe('Do not replay');
expect(store.getState().requestsByConversationId['conversation-a']['request-1'])