feat: integrate pixel teacher presence and resilient classroom preview
This commit is contained in:
@@ -665,6 +665,64 @@ describe('coding Conversation store', () => {
|
||||
expect(store.getState().draftsByConversationId['conversation-a'].text).toBe('');
|
||||
});
|
||||
|
||||
it.each(['completed', 'aborted', 'failed'] as const)('settles an accepted prompt from its own %s run without a request-tagged history node', async (terminalReason) => {
|
||||
const store = createCodingConversationStore({
|
||||
submitPrompt: vi.fn(async (input) => acceptance(input.conversationId, input.clientRequestId)),
|
||||
createId: ids(),
|
||||
});
|
||||
store.getState().applySnapshotEvent(snapshotEvent(snapshot('conversation-a')));
|
||||
store.getState().setDraft('conversation-a', 'hello');
|
||||
await store.getState().submitPrompt({ conversationId: 'conversation-a', mode: 'prompt' });
|
||||
store.getState().setDraft('conversation-a', '我的游戏在哪里哦');
|
||||
const settled = snapshot('conversation-a', 1, 1);
|
||||
settled.run = { status: terminalReason === 'failed' ? 'error' : 'idle', runId: 'run-1', terminalReason, settledAt: 2_000 };
|
||||
store.getState().applySnapshotEvent(snapshotEvent(settled));
|
||||
expect(store.getState().requestsByConversationId['conversation-a']).toEqual({});
|
||||
expect(selectCodingConversationSnapshot('conversation-a')(store.getState())?.nodes).toEqual(settled.nodes);
|
||||
expect(store.getState().draftsByConversationId['conversation-a'].text).toBe('我的游戏在哪里哦');
|
||||
});
|
||||
|
||||
it('settles a prompt when its terminal snapshot arrives before the HTTP acceptance', async () => {
|
||||
const pending = deferred<PromptAcceptance>();
|
||||
const store = createCodingConversationStore({ submitPrompt: vi.fn(() => pending.promise), createId: ids() });
|
||||
store.getState().applySnapshotEvent(snapshotEvent(snapshot('conversation-a')));
|
||||
store.getState().setDraft('conversation-a', 'hello');
|
||||
const submission = store.getState().submitPrompt({ conversationId: 'conversation-a', mode: 'prompt' });
|
||||
const settled = snapshot('conversation-a', 1, 1);
|
||||
settled.run = { status: 'idle', runId: 'run-1', terminalReason: 'completed', settledAt: 2_000 };
|
||||
store.getState().applySnapshotEvent(snapshotEvent(settled));
|
||||
expect(store.getState().requestsByConversationId['conversation-a']['request-1'].status).toBe('pending');
|
||||
store.getState().setDraft('conversation-a', '保留新的问题');
|
||||
pending.resolve(acceptance('conversation-a', 'request-1'));
|
||||
await submission;
|
||||
expect(store.getState().requestsByConversationId['conversation-a']).toEqual({});
|
||||
expect(selectCodingConversationSnapshot('conversation-a')(store.getState())?.nodes).toEqual(settled.nodes);
|
||||
expect(store.getState().draftsByConversationId['conversation-a'].text).toBe('保留新的问题');
|
||||
});
|
||||
|
||||
it('does not treat an old idle snapshot or a locally optimistic node in an unrelated patch as delivery evidence', async () => {
|
||||
const pending = deferred<PromptAcceptance>();
|
||||
const store = createCodingConversationStore({ submitPrompt: vi.fn(() => pending.promise), createId: ids() });
|
||||
store.getState().applySnapshotEvent(snapshotEvent(snapshot('conversation-a')));
|
||||
store.getState().setDraft('conversation-a', 'hello');
|
||||
const submission = store.getState().submitPrompt({ conversationId: 'conversation-a', mode: 'prompt' });
|
||||
const previous = snapshot('conversation-a', 1, 1);
|
||||
previous.run = { status: 'idle', runId: 'previous-run', terminalReason: 'completed', settledAt: 1_000 };
|
||||
store.getState().applySnapshotEvent(snapshotEvent(previous));
|
||||
store.getState().applyPatchBatchEvent(patchEvent('conversation-a', 2, { op: 'worker.state', state: { status: 'ready', generation: 1 } }));
|
||||
expect(store.getState().requestsByConversationId['conversation-a']['request-1'].status).toBe('pending');
|
||||
pending.resolve(acceptance('conversation-a', 'request-1'));
|
||||
await submission;
|
||||
expect(store.getState().requestsByConversationId['conversation-a']['request-1']).toMatchObject({ status: 'accepted', runId: 'run-1' });
|
||||
store.getState().applySnapshotEvent(snapshotEvent({ ...previous, cursor: { workerGeneration: 1, seq: 3 } }));
|
||||
expect(store.getState().requestsByConversationId['conversation-a']['request-1'].status).toBe('accepted');
|
||||
store.getState().applyPatchBatchEvent(patchEvent('conversation-a', 4, { op: 'run.state', run: { status: 'idle', runId: 'run-1' } }));
|
||||
expect(store.getState().requestsByConversationId['conversation-a']['request-1'].status).toBe('accepted');
|
||||
store.getState().applyPatchBatchEvent(patchEvent('conversation-a', 5, { op: 'run.state', run: { status: 'idle', runId: 'run-1', terminalReason: 'completed', settledAt: 2_000 } }));
|
||||
expect(store.getState().requestsByConversationId['conversation-a']).toEqual({});
|
||||
expect(selectCodingConversationSnapshot('conversation-a')(store.getState())?.nodes.some((node) => node.kind === 'message' && node.status === 'optimistic')).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves the optimistic UI id when a reconnect snapshot is already durable', async () => {
|
||||
const store = createCodingConversationStore({
|
||||
getSnapshot: vi.fn(),
|
||||
|
||||
Reference in New Issue
Block a user