fix: isolate delivered agent chats by project
This commit is contained in:
1 parent
a21a1f077c
commit
4b41c23a3a
15 files changed
+518
-165
No files matched your search
@@ -96,9 +96,10 @@ async function installCodingFirstChatHost(
|
||||
managedCapabilities = false,
|
||||
removedModel = false,
|
||||
audioPreview?: { executionId: string; path: string; dataUrl: string },
|
||||
multipleProjects = false,
|
||||
): Promise<void> {
|
||||
await electronApp.evaluate(async (_, payload) => {
|
||||
const { connection, featureComplete, managedCapabilities, removedModel, audioPreview } = payload;
|
||||
const { connection, featureComplete, managedCapabilities, removedModel, audioPreview, multipleProjects } = payload;
|
||||
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');
|
||||
type MainState = TeacherCatalogFixture & {
|
||||
captured: CapturedRequest[];
|
||||
@@ -139,6 +140,8 @@ async function installCodingFirstChatHost(
|
||||
updatedAt: now,
|
||||
lastOpenedAt: now,
|
||||
};
|
||||
const otherProject = { ...project, id: 'project-pi-other', name: 'Another project' };
|
||||
let activeProject = project;
|
||||
const configuredModel = {
|
||||
accountId: managedCapabilities ? 'niancode-user-models' : 'account-e2e',
|
||||
modelId: 'model-a',
|
||||
@@ -560,21 +563,21 @@ async function installCodingFirstChatHost(
|
||||
return respond(state.teacherCatalog);
|
||||
}
|
||||
if (path === '/api/coding/teacher/config') return respond({enabled:true,revision:1,published_version:1,definition:teacherDefinition});
|
||||
const agentChat = path.match(/^\/api\/coding\/agent-conversations\/([^/?]+)(?:\/(messages|seen))?(?:\?.*)?$/);
|
||||
const agentChat = path.match(/^\/api\/coding\/projects\/([^/?]+)\/agent-conversations\/([^/?]+)(?:\/(messages|seen))?(?:\?.*)?$/);
|
||||
if (agentChat) {
|
||||
const agentId = agentChat[1], action = agentChat[2];
|
||||
const projectId = agentChat[1], agentId = agentChat[2], action = agentChat[3], chatKey = projectId + ':' + agentId;
|
||||
const selected = state.teacherCatalog.items.find(item => item.teacher_id === agentId)!;
|
||||
const current = consultationTopics[agentId] ?? {schemaVersion:1,revision:0,id:'chat-' + agentId,accountId:'e2e',projectId:project.id,sourceConversationId:'project',definition:selected.definition,version:selected.version,createdAt:now,updatedAt:now,requests:[],conversation:{agentId,segmentTurns:0,discussions:{}}};
|
||||
if (!action && method === 'GET') return respond({topic: consultationTopics[agentId] ?? null, before:null});
|
||||
const current = consultationTopics[chatKey] ?? {schemaVersion:1,revision:0,id:'chat-' + chatKey,accountId:'e2e',projectId,sourceConversationId:'project',definition:selected?.definition ?? teacherDefinition,version:selected?.version ?? 1,createdAt:now,updatedAt:now,requests:[],conversation:{agentId,segmentTurns:0,discussions:{}}};
|
||||
if (!action && method === 'GET') return respond({topic: consultationTopics[chatKey] ?? null, before:null});
|
||||
if (action === 'seen') {
|
||||
consultationTopics[agentId] = {...current, revision:Number(current.revision)+1,conversation:{...current.conversation as object,seenThrough:body!.requestId}};
|
||||
return respond(consultationTopics[agentId]);
|
||||
consultationTopics[chatKey] = {...current, revision:Number(current.revision)+1,conversation:{...current.conversation as object,seenThrough:body!.requestId}};
|
||||
return respond(consultationTopics[chatKey]);
|
||||
}
|
||||
if (action === 'messages' && method === 'POST') {
|
||||
const suggestions = body!.intent === 'suggestions';
|
||||
const response = suggestions ? '我们可以从你最近试过的地方聊起。' : body!.intent === 'guided-help' ? '你最近做的哪一步,让你停下来想了一会儿?' : '先理解状态如何随点击变化,再修改代码。';
|
||||
consultationTopics[agentId] = {...current,revision:Number(current.revision)+1,requests:[...(current.requests as unknown[]),{id:body!.requestId,projectId:body!.projectId,sourceConversationId:body!.sourceConversationId,text:body!.text,intent:body!.intent,references:body!.references??[],createdAt:new Date().toISOString(),sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,truncatedMessages:1,status:'completed',response,...(suggestions?{suggestedQuestions:['怎样观察别人玩游戏?','我该先试哪个想法?']}:{})}]};
|
||||
return respond(consultationTopics[agentId],202);
|
||||
consultationTopics[chatKey] = {...current,revision:Number(current.revision)+1,requests:[...(current.requests as unknown[]),{id:body!.requestId,projectId,sourceConversationId:body!.sourceConversationId,text:body!.text,intent:body!.intent,references:body!.references??[],createdAt:new Date().toISOString(),sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,truncatedMessages:1,status:'completed',response,...(suggestions?{suggestedQuestions:['怎样观察别人玩游戏?','我该先试哪个想法?']}:{})}]};
|
||||
return respond(consultationTopics[chatKey],202);
|
||||
}
|
||||
}
|
||||
const consultationPath = path.match(/\/(agent|teacher)-topics(?:\/|$)/)?.[1];
|
||||
@@ -597,8 +600,19 @@ async function installCodingFirstChatHost(
|
||||
if (path.endsWith(`${topicBase}/${topicId}`)) return respond(currentTopic);
|
||||
}
|
||||
if (path === '/api/coding/projects') {
|
||||
return respond({ projects: [project], activeProjectId: project.id });
|
||||
return respond({ projects: multipleProjects ? [project, otherProject] : [project], activeProjectId: activeProject.id });
|
||||
}
|
||||
if (path === '/api/coding/projects/active' && method === 'POST') {
|
||||
activeProject = body!.projectId === otherProject.id ? otherProject : project;
|
||||
return respond({ project: activeProject });
|
||||
}
|
||||
if (multipleProjects && path === `/api/coding/projects/config?projectId=${otherProject.id}`)
|
||||
return respond({ snapshot: { project: otherProject, config, knowledgeFiles: [] } });
|
||||
if (multipleProjects && path === `/api/coding/projects/conversations?projectId=${otherProject.id}`)
|
||||
return respond({ conversations: [{ ...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 } } });
|
||||
if (path === '/api/provider-accounts') {
|
||||
return respond([{
|
||||
id: managedCapabilities ? 'niancode-user-models' : 'account-e2e',
|
||||
@@ -836,7 +850,7 @@ async function installCodingFirstChatHost(
|
||||
if (path === '/api/coding/runtime/diagnostics') return respond({ runtime: { revision: { provider: 1, resources: 1 }, workers: [{ conversationId: conversation.id, generation: 1, state: 'running', stage: 'running' }] } });
|
||||
return respond({ success: false, error: `Unhandled E2E route: ${method} ${path}` }, 404);
|
||||
});
|
||||
}, { connection: hostConnection, featureComplete, managedCapabilities, removedModel, audioPreview });
|
||||
}, { connection: hostConnection, featureComplete, managedCapabilities, removedModel, audioPreview, multipleProjects });
|
||||
}
|
||||
|
||||
test('saved Game Audio offers click-only playable local preview in Electron', async ({ launchElectronApp }, testInfo) => {
|
||||
@@ -1888,7 +1902,7 @@ test('project consultations preserve student drafts and switch between work and
|
||||
const consultationRequests=requests.filter(item=>item.path.endsWith('/messages')&&item.method==='POST');
|
||||
expect(consultationRequests).toHaveLength(3);
|
||||
expect(requests.filter(item => item.path.endsWith('/agent-topics') && item.method === 'POST')).toHaveLength(0);
|
||||
expect(consultationRequests.every(item => item.path === '/api/coding/agent-conversations/teacher-algorithm/messages')).toBe(true);
|
||||
expect(consultationRequests.every(item => item.path === '/api/coding/projects/project-pi-first-chat/agent-conversations/teacher-algorithm/messages')).toBe(true);
|
||||
await expect(teacher.getByRole('button', { name: '智能体新话题' })).toHaveCount(0);
|
||||
await expect(teacher.getByRole('combobox')).toHaveCount(0);
|
||||
await expect(teacher).toHaveAccessibleName('朋友咨询');
|
||||
@@ -1905,6 +1919,56 @@ test('project consultations preserve student drafts and switch between work and
|
||||
});
|
||||
|
||||
|
||||
test('the same delivered agent keeps separate project chats and drafts across project switches and reload', async ({ launchElectronApp }) => {
|
||||
const electronApp = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(electronApp);
|
||||
const connection = await page.evaluate(async () => ({
|
||||
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
|
||||
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
|
||||
}));
|
||||
await installCodingFirstChatHost(electronApp, connection, true, false, false, undefined, true);
|
||||
await settleSnapshot(electronApp);
|
||||
await disableCodingEventSource(page);
|
||||
try {
|
||||
await page.reload(); page = await getStableWindow(electronApp);
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await page.evaluate(() => { window.location.hash = '/chat'; });
|
||||
await expect(page.getByTestId('project-conversations')).toBeVisible();
|
||||
await page.getByRole('button', { name: '与代码智能体聊天', exact: true }).click();
|
||||
const panel = page.getByTestId('teacher-chat-panel');
|
||||
const input = panel.getByRole('textbox', { name: '向智能体提问' });
|
||||
await input.fill('天气项目的独立问题');
|
||||
await panel.getByRole('button', { name: '提问', exact: true }).click();
|
||||
await expect(panel.getByText('天气项目的独立问题', { exact: true })).toBeVisible();
|
||||
await input.fill('天气项目还未发送');
|
||||
await page.getByRole('button', { name: '进入项目 Another project', exact: true }).click();
|
||||
await expect(page.getByTestId('project-conversations').getByText('Other project conversation', { exact: true })).toBeVisible();
|
||||
// Switching a project may close the consultation dock; reopening reads this project's chat only.
|
||||
if (!await panel.isVisible()) await page.getByRole('button', { name: '与代码智能体聊天', exact: true }).click();
|
||||
await expect(input).toHaveValue('');
|
||||
await expect(panel.getByText('天气项目的独立问题', { exact: true })).toHaveCount(0);
|
||||
await input.fill('游戏项目的独立问题');
|
||||
await panel.getByRole('button', { name: '提问', exact: true }).click();
|
||||
await expect(panel.getByText('游戏项目的独立问题', { exact: true })).toBeVisible();
|
||||
await input.fill('游戏项目还未发送');
|
||||
await page.getByRole('button', { name: '进入项目 PI first chat', exact: true }).click();
|
||||
if (!await panel.isVisible()) await page.getByRole('button', { name: '与代码智能体聊天', exact: true }).click();
|
||||
await expect(input).toHaveValue('天气项目还未发送');
|
||||
await expect(panel.getByText('天气项目的独立问题', { exact: true })).toBeVisible();
|
||||
await expect(panel.getByText('游戏项目的独立问题', { exact: true })).toHaveCount(0);
|
||||
await page.reload(); page = await getStableWindow(electronApp);
|
||||
if (!await panel.isVisible()) await page.getByRole('button', { name: '与代码智能体聊天', exact: true }).click();
|
||||
await expect(input).toHaveValue('天气项目还未发送');
|
||||
await expect(panel.getByText('天气项目的独立问题', { exact: true })).toBeVisible();
|
||||
await expect(panel.getByRole('combobox')).toHaveCount(0);
|
||||
const sends = (await readState(electronApp)).captured.filter(item => item.path.endsWith('/messages') && item.method === 'POST');
|
||||
expect(sends.map(item => item.body?.projectId)).toEqual(['project-pi-first-chat', 'project-pi-other']);
|
||||
expect(sends[0].path).toBe('/api/coding/projects/project-pi-first-chat/agent-conversations/teacher-code/messages');
|
||||
expect(sends[1].path).toBe('/api/coding/projects/project-pi-other/agent-conversations/teacher-code/messages');
|
||||
await page.screenshot({ path: test.info().outputPath('project-scoped-agent-chat.png') });
|
||||
} finally { await releaseSnapshot(electronApp); }
|
||||
});
|
||||
|
||||
test('manual agent refresh updates the catalog and retries failures without replacing the ongoing chat or drafts', async ({ launchElectronApp }) => {
|
||||
const electronApp = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(electronApp);
|
||||
|
||||
Reference in new issue
Block a user