chore(integration): snapshot local main worktree

This commit is contained in:
inman
2026-08-31 10:23:21 +08:00
parent 48a9189939
commit 33fb31fb28
116 changed files with 8108 additions and 3026 deletions

View File

@@ -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',