chore(integration): snapshot local main worktree
This commit is contained in:
@@ -21,6 +21,28 @@ function deferred<T>() {
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
function withForkSource(
|
||||
snapshot: ConversationSnapshot,
|
||||
sourceEntryId: string,
|
||||
): ConversationSnapshot {
|
||||
return {
|
||||
...snapshot,
|
||||
nodes: [{
|
||||
kind: 'message',
|
||||
id: `message-${sourceEntryId}`,
|
||||
sourceEntryId,
|
||||
role: 'user',
|
||||
status: 'complete',
|
||||
blocks: [{
|
||||
kind: 'text',
|
||||
id: `text-${sourceEntryId}`,
|
||||
text: 'Fork source',
|
||||
status: 'complete',
|
||||
}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
const runtimeSnapshot = deferred<ConversationSnapshot>();
|
||||
|
||||
class FakeEventSource {
|
||||
@@ -67,7 +89,6 @@ const config: CodingProjectConfig = {
|
||||
initialized: true,
|
||||
agents: [agent],
|
||||
knowledgeDirectory: 'knowledge',
|
||||
legacyConversationNotice: 'none',
|
||||
createdAt: '2026-08-23T00:00:00.000Z',
|
||||
updatedAt: '2026-08-23T00:00:00.000Z',
|
||||
};
|
||||
@@ -109,7 +130,6 @@ const projectApi = vi.hoisted(() => ({
|
||||
createProject: vi.fn(),
|
||||
setActive: vi.fn(),
|
||||
removeProject: vi.fn(),
|
||||
acknowledgeNotice: vi.fn(),
|
||||
}));
|
||||
const conversationApi = vi.hoisted(() => ({
|
||||
snapshot: vi.fn(),
|
||||
@@ -119,7 +139,6 @@ const conversationApi = vi.hoisted(() => ({
|
||||
abort: vi.fn(),
|
||||
model: vi.fn(),
|
||||
thinking: vi.fn(),
|
||||
compact: vi.fn(),
|
||||
fork: vi.fn(),
|
||||
respond: vi.fn(),
|
||||
diagnostics: vi.fn(),
|
||||
@@ -136,7 +155,6 @@ vi.mock('@/lib/coding-projects', () => ({
|
||||
createCodingProject: projectApi.createProject,
|
||||
setActiveCodingProject: projectApi.setActive,
|
||||
removeCodingProject: projectApi.removeProject,
|
||||
acknowledgeLegacyCodingConversationNotice: projectApi.acknowledgeNotice,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/coding-conversations', () => ({
|
||||
@@ -147,7 +165,6 @@ vi.mock('@/lib/coding-conversations', () => ({
|
||||
abortCodingConversation: conversationApi.abort,
|
||||
setCodingConversationModel: conversationApi.model,
|
||||
setCodingConversationThinking: conversationApi.thinking,
|
||||
compactCodingConversation: conversationApi.compact,
|
||||
forkCodingConversation: conversationApi.fork,
|
||||
respondCodingConversationInteraction: conversationApi.respond,
|
||||
getCodingRuntimeDiagnostics: conversationApi.diagnostics,
|
||||
@@ -302,7 +319,7 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
expect(screen.queryByText(conversation.title)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not let a pending fork steal selection or Header busy state after a Conversation switch', async () => {
|
||||
it('does not let a pending message fork steal selection after a Conversation switch', async () => {
|
||||
const forkFlight = deferred<CodingConversationMetadata>();
|
||||
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
|
||||
projectApi.config.mockResolvedValue({ project, config: configForAgents([agent, reviewer]) });
|
||||
@@ -314,21 +331,22 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
|
||||
const { codingConversationStore } = await import('@/stores/coding-conversations');
|
||||
conversationApi.snapshot.mockImplementation(async (conversationId: string) => (
|
||||
createLocalConversationSnapshot(
|
||||
withForkSource(createLocalConversationSnapshot(
|
||||
project.id,
|
||||
conversationId === reviewerConversation.id ? reviewerConversation : conversation,
|
||||
)
|
||||
), `entry-${conversationId}`)
|
||||
));
|
||||
render(<CodingChatPanel />);
|
||||
await waitFor(() => expect(codingConversationStore.getState().selectedConversationId)
|
||||
.toBe(conversation.id));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建分支' }));
|
||||
await waitFor(() => expect(conversationApi.fork).toHaveBeenCalledWith(conversation.id, undefined));
|
||||
fireEvent.click(screen.getByRole('button', { name: '从这里创建新对话分支' }));
|
||||
await waitFor(() => expect(conversationApi.fork)
|
||||
.toHaveBeenCalledWith(conversation.id, `entry-${conversation.id}`));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Reviewer/ }));
|
||||
await waitFor(() => expect(codingConversationStore.getState().selectedConversationId)
|
||||
.toBe(reviewerConversation.id));
|
||||
expect(screen.getByRole('button', { name: '创建分支' })).toBeEnabled();
|
||||
expect(screen.getByRole('button', { name: '从这里创建新对话分支' })).toBeEnabled();
|
||||
|
||||
await act(async () => {
|
||||
forkFlight.resolve({
|
||||
@@ -362,17 +380,18 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
const { codingWorkspaceStore } = await import('@/stores/coding-workspace');
|
||||
const { codingConversationStore } = await import('@/stores/coding-conversations');
|
||||
conversationApi.snapshot.mockImplementation(async (conversationId: string) => (
|
||||
createLocalConversationSnapshot(
|
||||
withForkSource(createLocalConversationSnapshot(
|
||||
conversationId === secondConversation.id ? secondProject.id : project.id,
|
||||
conversationId === secondConversation.id ? secondConversation : conversation,
|
||||
)
|
||||
), `entry-${conversationId}`)
|
||||
));
|
||||
render(<CodingChatPanel />);
|
||||
await waitFor(() => expect(codingConversationStore.getState().selectedConversationId)
|
||||
.toBe(conversation.id));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建分支' }));
|
||||
await waitFor(() => expect(conversationApi.fork).toHaveBeenCalledWith(conversation.id, undefined));
|
||||
fireEvent.click(screen.getByRole('button', { name: '从这里创建新对话分支' }));
|
||||
await waitFor(() => expect(conversationApi.fork)
|
||||
.toHaveBeenCalledWith(conversation.id, `entry-${conversation.id}`));
|
||||
act(() => {
|
||||
codingWorkspaceStore.setState({
|
||||
activeProjectId: secondProject.id,
|
||||
@@ -426,12 +445,10 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
expect(screen.queryByText('metadata rejected')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not clear a newly selected Conversation when an old archive request finishes', async () => {
|
||||
const archiveFlight = deferred<CodingConversationMetadata>();
|
||||
it('keeps project settings only in the Conversation list and removes legacy Header actions', async () => {
|
||||
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
|
||||
projectApi.config.mockResolvedValue({ project, config: configForAgents([agent, reviewer]) });
|
||||
projectApi.conversations.mockResolvedValue([conversation, reviewerConversation]);
|
||||
projectApi.patch.mockReturnValue(archiveFlight.promise);
|
||||
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
|
||||
conversationApi.recover.mockResolvedValue(undefined);
|
||||
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
|
||||
@@ -443,22 +460,20 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
conversationId === reviewerConversation.id ? reviewerConversation : conversation,
|
||||
)
|
||||
));
|
||||
render(<CodingChatPanel />);
|
||||
const onOpenProjectSettings = vi.fn();
|
||||
render(<CodingChatPanel onOpenProjectSettings={onOpenProjectSettings} />);
|
||||
await waitFor(() => expect(codingConversationStore.getState().selectedConversationId)
|
||||
.toBe(conversation.id));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '归档' }));
|
||||
await waitFor(() => expect(projectApi.patch).toHaveBeenCalledWith(conversation.id, { archived: true }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Reviewer/ }));
|
||||
await waitFor(() => expect(codingConversationStore.getState().selectedConversationId)
|
||||
.toBe(reviewerConversation.id));
|
||||
expect(screen.getByRole('button', { name: '归档' })).toBeEnabled();
|
||||
const header = screen.getByTestId('coding-conversation-header');
|
||||
expect(within(header).queryByRole('button', { name: '创建分支' })).not.toBeInTheDocument();
|
||||
expect(within(header).queryByRole('button', { name: /标为(已读|未读)/ })).not.toBeInTheDocument();
|
||||
expect(within(header).queryByRole('button', { name: '归档' })).not.toBeInTheDocument();
|
||||
expect(within(header).queryByRole('button', { name: '伙伴设置' })).not.toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button', { name: '项目设置' })).toHaveLength(1);
|
||||
|
||||
await act(async () => {
|
||||
archiveFlight.resolve({ ...conversation, archivedAt: '2026-08-24T01:00:00.000Z' });
|
||||
await archiveFlight.promise;
|
||||
});
|
||||
expect(codingConversationStore.getState().selectedConversationId).toBe(reviewerConversation.id);
|
||||
fireEvent.click(screen.getByRole('button', { name: '项目设置' }));
|
||||
expect(onOpenProjectSettings).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('remounts pending interaction state when switching Conversations', async () => {
|
||||
@@ -648,7 +663,7 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
},
|
||||
}],
|
||||
}));
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '整理上下文' })).toBeDisabled());
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: /模型与思考设置/ })).toBeDisabled());
|
||||
|
||||
act(() => codingConversationStore.getState().applyPatchBatchEvent({
|
||||
type: 'patch-batch',
|
||||
|
||||
Reference in New Issue
Block a user