feat: replace Code conversation archive with deletion
This commit is contained in:
1 parent
140f6be8db
commit
bf101c1e52
19 files changed
+1001
-292
No files matched your search
@@ -120,6 +120,9 @@ async function installCodingFirstChatHost(
|
||||
releaseSnapshot: (() => void) | null;
|
||||
snapshotPending: boolean;
|
||||
snapshotSettled: boolean;
|
||||
deletedConversationIds: string[];
|
||||
deleteFailureId?: string;
|
||||
legacyArchivedConversationId?: string;
|
||||
workReady?: boolean;
|
||||
workFailed?: boolean;
|
||||
workGeneration?: number;
|
||||
@@ -136,6 +139,7 @@ async function installCodingFirstChatHost(
|
||||
releaseSnapshot: null,
|
||||
snapshotPending: false,
|
||||
snapshotSettled: false,
|
||||
deletedConversationIds: [],
|
||||
teacherCatalog: { items: [] },
|
||||
teacherCatalogFailure: false,
|
||||
holdTeacherCatalog: false,
|
||||
@@ -525,6 +529,20 @@ async function installCodingFirstChatHost(
|
||||
at: Date.now(),
|
||||
});
|
||||
|
||||
if (/^\/api\/coding\/conversations\/[^/]+$/.test(path) && method === 'DELETE') {
|
||||
const id = decodeURIComponent(path.slice(path.lastIndexOf('/') + 1));
|
||||
const target = [conversation.id, secondConversation.id, historyConversation.id,
|
||||
...(multipleProjects ? ['conversation-other-project'] : [])].includes(id);
|
||||
if (!target || state.deletedConversationIds.includes(id)) return respond({ error: '对话不存在' }, 404);
|
||||
if (state.deleteFailureId === id) return respond({ error: '删除失败,请重试' }, 500);
|
||||
state.deletedConversationIds.push(id);
|
||||
metadata.delete(id);
|
||||
return respond({ success: true });
|
||||
}
|
||||
if (state.deletedConversationIds.some(id => path === `/api/coding/conversations/${id}/snapshot`)) {
|
||||
return respond({ error: '对话不存在' }, 404);
|
||||
}
|
||||
|
||||
if (path === '/api/coding/attachments'
|
||||
|| /^\/api\/coding\/attachments\/[^/]+\/content$/.test(path)) {
|
||||
const response = await fetch(`${connection.baseUrl}${path}`, {
|
||||
@@ -636,7 +654,8 @@ async function installCodingFirstChatHost(
|
||||
config: { ...config, defaultAgentId: agent.id, agents: [{ ...agent,
|
||||
model: { ...configuredModel, modelId: 'model-b' }, prompt: 'Settings for Another project' }] }, knowledgeFiles: [] } });
|
||||
if (multipleProjects && path === `/api/coding/projects/conversations?projectId=${otherProject.id}`)
|
||||
return respond({ conversations: [{ ...secondConversation, id: 'conversation-other-project', title: 'Other project conversation' }] });
|
||||
return respond({ conversations: state.deletedConversationIds.includes('conversation-other-project') ? []
|
||||
: [{ ...secondConversation, id: 'conversation-other-project', title: 'Other project conversation' }] });
|
||||
if (multipleProjects && path === '/api/coding/conversations/conversation-other-project/snapshot')
|
||||
return respond({ snapshot: { ...secondSnapshot, conversation: { ...secondSnapshot.conversation,
|
||||
id: 'conversation-other-project', projectId: otherProject.id } } });
|
||||
@@ -674,7 +693,8 @@ async function installCodingFirstChatHost(
|
||||
? [conversation, secondConversation, historyConversation]
|
||||
: state.conversationCreated
|
||||
? [conversation]
|
||||
: []).map((item) => ({ ...item, ...metadata.get(item.id) })),
|
||||
: []).filter(item => !state.deletedConversationIds.includes(item.id)).map((item) => ({ ...item,
|
||||
...(state.legacyArchivedConversationId === item.id ? { archivedAt: now } : {}), ...metadata.get(item.id) })),
|
||||
});
|
||||
}
|
||||
if (path.startsWith('/api/agent-browser/') && mainGlobal.__makeloreNativeBrowserE2E) {
|
||||
@@ -945,17 +965,20 @@ test('saved Game Audio offers click-only playable local preview in Electron', as
|
||||
async function readState(electronApp: ElectronApplication): Promise<{
|
||||
captured: CapturedRequest[];
|
||||
snapshotPending: boolean;
|
||||
deletedConversationIds: string[];
|
||||
}> {
|
||||
return await electronApp.evaluate(() => {
|
||||
const mainGlobal = globalThis as typeof globalThis & {
|
||||
__makelorePiFirstChatE2E?: {
|
||||
captured: CapturedRequest[];
|
||||
snapshotPending: boolean;
|
||||
deletedConversationIds: string[];
|
||||
};
|
||||
};
|
||||
return structuredClone({
|
||||
captured: mainGlobal.__makelorePiFirstChatE2E?.captured ?? [],
|
||||
snapshotPending: mainGlobal.__makelorePiFirstChatE2E?.snapshotPending ?? false,
|
||||
deletedConversationIds: mainGlobal.__makelorePiFirstChatE2E?.deletedConversationIds ?? [],
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -980,7 +1003,7 @@ async function settleSnapshot(electronApp: ElectronApplication): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
test('conversation menus rename, archive and restore without selecting or stopping the target', async ({ launchElectronApp }) => {
|
||||
test('conversation menus rename and delete exact targets while preserving other conversations and failed deletions', async ({ launchElectronApp }) => {
|
||||
const electronApp = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(electronApp);
|
||||
const connection = await page.evaluate(async () => ({
|
||||
@@ -988,7 +1011,10 @@ test('conversation menus rename, archive and restore without selecting or stoppi
|
||||
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
|
||||
}));
|
||||
await installCodingFirstChatHost(electronApp, connection, true);
|
||||
await settleSnapshot(electronApp);
|
||||
await electronApp.evaluate(() => {
|
||||
const state = (globalThis as typeof globalThis & { __makelorePiFirstChatE2E: { legacyArchivedConversationId: string } }).__makelorePiFirstChatE2E;
|
||||
state.legacyArchivedConversationId = 'conversation-pi-history';
|
||||
});
|
||||
await disableCodingEventSource(page);
|
||||
try {
|
||||
await page.reload();
|
||||
@@ -997,12 +1023,15 @@ test('conversation menus rename, archive and restore without selecting or stoppi
|
||||
await page.evaluate(() => { window.location.hash = '/chat'; });
|
||||
const sidebar = page.getByTestId('project-conversations');
|
||||
const header = page.getByTestId('coding-conversation-header');
|
||||
const projectMenu = page.getByRole('button', { name: '项目操作:PI first chat', exact: true });
|
||||
const current = sidebar.getByRole('button', { name: '新对话', exact: true });
|
||||
const legacy = sidebar.getByRole('button', { name: 'History and quota', exact: true });
|
||||
await expect(header).toContainText('新对话');
|
||||
await expect(sidebar.getByRole('button', { name: '查看归档对话' })).toHaveCount(0);
|
||||
await expect(sidebar.getByRole('button', { name: '项目设置', exact: true })).toHaveCount(0);
|
||||
await expect(current.getByLabel('对话正在运行')).toBeVisible();
|
||||
await expect(legacy).toBeVisible();
|
||||
await sidebar.getByRole('button', { name: '对话操作:Second Conversation' }).focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.getByRole('menuitem')).toHaveText(['重命名', '删除对话']);
|
||||
await page.screenshot({ path: test.info().outputPath('conversation-actions-delete.png'), animations: 'disabled' });
|
||||
await page.getByRole('menuitem', { name: '重命名', exact: true }).click();
|
||||
const title = page.getByRole('textbox', { name: '对话标题' });
|
||||
await expect(title).toHaveValue('Second Conversation');
|
||||
@@ -1012,35 +1041,51 @@ test('conversation menus rename, archive and restore without selecting or stoppi
|
||||
await expect(header).toContainText('新对话');
|
||||
await sidebar.getByRole('button', { name: '对话操作:重命名后的会话' }).focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await page.getByRole('menuitem', { name: '归档', exact: true }).click();
|
||||
await page.getByRole('menuitem', { name: '删除对话', exact: true }).click();
|
||||
await expect(sidebar.getByRole('button', { name: '重命名后的会话', exact: true })).toHaveCount(0);
|
||||
await projectMenu.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await page.getByRole('menuitem', { name: '已归档对话', exact: true }).click();
|
||||
await expect(sidebar.getByText('已归档对话', { exact: true })).toBeVisible();
|
||||
await expect(sidebar.getByRole('button', { name: '返回最近对话', exact: true })).toBeVisible();
|
||||
await expect(header).toContainText('新对话');
|
||||
await projectMenu.click();
|
||||
await expect(page.getByRole('menuitem', { name: '最近对话', exact: true })).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
await sidebar.getByRole('button', { name: '重命名后的会话', exact: true }).click();
|
||||
await expect(page.getByRole('button', { name: '恢复对话', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '发送', exact: true })).toHaveCount(0);
|
||||
await page.getByRole('button', { name: '恢复对话', exact: true }).click();
|
||||
await expect(header).toContainText('重命名后的会话');
|
||||
await expect(sidebar.getByRole('button', { name: '返回最近对话', exact: true })).toHaveCount(0);
|
||||
await expect(sidebar.getByRole('button', { name: '重命名后的会话', exact: true })).toBeVisible();
|
||||
await projectMenu.click();
|
||||
await page.getByRole('menuitem', { name: '已归档对话', exact: true }).click();
|
||||
await sidebar.getByRole('button', { name: '返回最近对话', exact: true }).click();
|
||||
await expect(sidebar.getByRole('button', { name: '重命名后的会话', exact: true })).toBeVisible();
|
||||
const requests = (await readState(electronApp)).captured;
|
||||
expect(requests.some((request) => request.path.endsWith('/abort'))).toBe(false);
|
||||
expect(requests.filter((request) => request.method === 'PATCH' && request.path.endsWith('/conversation-pi-second'))
|
||||
.map((request) => request.body)).toEqual([
|
||||
{ title: '重命名后的会话' }, { archived: true }, { archived: false },
|
||||
await expect(current.getByLabel('对话正在运行')).toBeVisible();
|
||||
await expect(page.getByRole('dialog')).toHaveCount(0);
|
||||
expect((await readState(electronApp)).deletedConversationIds).toEqual(['conversation-pi-second']);
|
||||
|
||||
await electronApp.evaluate(() => {
|
||||
const state = (globalThis as typeof globalThis & { __makelorePiFirstChatE2E: { deleteFailureId: string } }).__makelorePiFirstChatE2E;
|
||||
state.deleteFailureId = 'conversation-pi-history';
|
||||
});
|
||||
await sidebar.getByRole('button', { name: '对话操作:History and quota' }).click();
|
||||
await page.getByRole('menuitem', { name: '删除对话', exact: true }).click();
|
||||
await expect(page.getByText('删除失败,请重试', { exact: true })).toBeVisible();
|
||||
await expect(legacy).toBeVisible();
|
||||
await expect(header).toContainText('新对话');
|
||||
expect((await readState(electronApp)).deletedConversationIds).toEqual(['conversation-pi-second']);
|
||||
|
||||
// Legacy records remain in the normal list and become editable on open.
|
||||
await legacy.click();
|
||||
await expect(header).toContainText('History and quota');
|
||||
await expect(page.getByRole('button', { name: '恢复对话', exact: true })).toHaveCount(0);
|
||||
expect((await readState(electronApp)).captured.filter(request => request.method === 'PATCH'
|
||||
&& request.path.endsWith('/conversation-pi-history')).map(request => request.body)).toEqual([{ archived: false }]);
|
||||
await current.click();
|
||||
await expect(current.getByLabel('对话正在运行')).toBeVisible();
|
||||
await sidebar.getByRole('button', { name: '对话操作:新对话' }).click();
|
||||
await page.getByRole('menuitem', { name: '删除对话', exact: true }).click();
|
||||
await expect(page.getByRole('heading', { name: '今天,你想创造什么?' })).toBeVisible();
|
||||
await expect(current).toHaveCount(0);
|
||||
await expect(legacy).toBeVisible();
|
||||
await expect(legacy).not.toHaveAttribute('aria-current', 'page');
|
||||
await expect(page.getByRole('dialog')).toHaveCount(0);
|
||||
const state = await readState(electronApp);
|
||||
expect(state.deletedConversationIds).toEqual(['conversation-pi-second', 'conversation-pi-first-chat']);
|
||||
expect(state.captured.filter(request => request.method === 'DELETE').map(request => request.path)).toEqual([
|
||||
'/api/coding/conversations/conversation-pi-second', '/api/coding/conversations/conversation-pi-history',
|
||||
'/api/coding/conversations/conversation-pi-first-chat',
|
||||
]);
|
||||
await page.screenshot({ path: test.info().outputPath('coding-session-controls.png') });
|
||||
expect(state.captured.filter(request => request.method === 'PATCH' && request.path.endsWith('/conversation-pi-second'))
|
||||
.map(request => request.body)).toEqual([{ title: '重命名后的会话' }]);
|
||||
expect(state.captured.some(request => request.path.endsWith('/abort'))).toBe(false);
|
||||
expect(state.captured.some(request => request.method === 'POST' && request.path === '/api/coding/projects/conversations')).toBe(false);
|
||||
await expect(page.getByText('删除失败,请重试', { exact: true })).toHaveCount(0);
|
||||
await page.screenshot({ path: test.info().outputPath('deleted-current-conversation.png'), animations: 'disabled' });
|
||||
} finally { await releaseSnapshot(electronApp); }
|
||||
});
|
||||
|
||||
@@ -1101,8 +1146,8 @@ test('project menus target their project and keep new conversations aligned at e
|
||||
await sidebarToggle.hover();
|
||||
await expect(rail).toHaveAttribute('aria-hidden', 'false');
|
||||
await primaryMenu.click();
|
||||
const archivedMenuItem = page.getByRole('menuitem', { name: '已归档对话', exact: true });
|
||||
await expect(archivedMenuItem).toBeVisible();
|
||||
const settingsMenuItem = page.getByRole('menuitem', { name: '项目设置', exact: true });
|
||||
await expect(settingsMenuItem).toBeVisible();
|
||||
await page.mouse.move(800, 500);
|
||||
// Stay outside the rail longer than its 180ms peek-close delay. An open
|
||||
// portal menu must keep the preview available and remain above it.
|
||||
@@ -1111,18 +1156,16 @@ test('project menus target their project and keep new conversations aligned at e
|
||||
// menu is open, so measure the rail's visual state here instead.
|
||||
await expect(rail).toHaveCSS('width', '256px');
|
||||
await expect(rail).toHaveCSS('opacity', '1');
|
||||
await expect(archivedMenuItem).toBeVisible();
|
||||
await archivedMenuItem.click({ trial: true });
|
||||
await expect(settingsMenuItem).toBeVisible();
|
||||
await settingsMenuItem.click({ trial: true });
|
||||
await page.screenshot({ path: test.info().outputPath('sidebar-project-menu-peek.png') });
|
||||
await archivedMenuItem.click();
|
||||
await expect(archivedMenuItem).toHaveCount(0);
|
||||
await expect(conversations.getByRole('button', { name: '返回最近对话', exact: true })).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(settingsMenuItem).toHaveCount(0);
|
||||
// Resume pointer tracking after the modal portal has released the page.
|
||||
await primaryMenu.hover();
|
||||
await page.mouse.move(800, 500);
|
||||
await expect(rail).toHaveAttribute('aria-hidden', 'true');
|
||||
await sidebarToggle.hover();
|
||||
await conversations.getByRole('button', { name: '返回最近对话', exact: true }).click();
|
||||
await sidebarToggle.click();
|
||||
await expect(rail).toHaveCSS('width', '256px');
|
||||
await expect(resizer).toBeVisible();
|
||||
@@ -1132,8 +1175,7 @@ test('project menus target their project and keep new conversations aligned at e
|
||||
const activationsBeforeMenu = await activationRequests();
|
||||
await otherMenu.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.getByRole('menuitem', { name: '已归档对话', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: '项目设置', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem')).toHaveText(['项目设置']);
|
||||
await expect(primaryProject).toHaveAttribute('aria-current', 'page');
|
||||
await expect(otherProject).not.toHaveAttribute('aria-current', 'page');
|
||||
await expect(page.getByTestId('coding-conversation-header')).toContainText('新对话');
|
||||
@@ -1158,16 +1200,14 @@ test('project menus target their project and keep new conversations aligned at e
|
||||
await page.screenshot({ path: test.info().outputPath('project-settings-model-only.png') });
|
||||
expect((await activationRequests()).at(-1)?.body).toEqual({ projectId: 'project-pi-other' });
|
||||
|
||||
// The archive action also validates and activates an inactive project.
|
||||
// Opening settings for another project keeps the action project-scoped.
|
||||
await primaryMenu.click();
|
||||
await page.getByRole('menuitem', { name: '已归档对话', exact: true }).click();
|
||||
await expect(page.getByTestId('coding-conversation-header')).toBeVisible();
|
||||
await page.getByRole('menuitem', { name: '项目设置', exact: true }).click();
|
||||
await expect(configuration).toBeVisible();
|
||||
await expect(primaryProject).toHaveAttribute('aria-current', 'page');
|
||||
await expect(conversations.getByText('已归档对话', { exact: true })).toBeVisible();
|
||||
expect((await activationRequests()).at(-1)?.body).toEqual({ projectId: 'project-pi-first-chat' });
|
||||
await primaryMenu.click();
|
||||
await page.getByRole('menuitem', { name: '最近对话', exact: true }).click();
|
||||
await expect(conversations.getByRole('button', { name: '返回最近对话', exact: true })).toHaveCount(0);
|
||||
await primaryProject.click();
|
||||
await expect(page.getByTestId('coding-conversation-header')).toBeVisible();
|
||||
await expect(conversations.getByRole('button', { name: '新对话', exact: true })).toBeVisible();
|
||||
expect((await readState(electronApp)).captured.filter(request => request.method === 'POST'
|
||||
&& /\/(prompt|messages|abort)$/.test(request.path))).toHaveLength(0);
|
||||
|
||||
Reference in new issue
Block a user