|
|
|
|
@@ -62,6 +62,7 @@ async function installCodingFirstChatHost(
|
|
|
|
|
type MainState = {
|
|
|
|
|
captured: CapturedRequest[];
|
|
|
|
|
conversationCreated: boolean;
|
|
|
|
|
interactionAnswered: boolean;
|
|
|
|
|
releaseSnapshot: (() => void) | null;
|
|
|
|
|
snapshotPending: boolean;
|
|
|
|
|
};
|
|
|
|
|
@@ -71,6 +72,7 @@ async function installCodingFirstChatHost(
|
|
|
|
|
const state: MainState = {
|
|
|
|
|
captured: [],
|
|
|
|
|
conversationCreated: false,
|
|
|
|
|
interactionAnswered: false,
|
|
|
|
|
releaseSnapshot: null,
|
|
|
|
|
snapshotPending: false,
|
|
|
|
|
};
|
|
|
|
|
@@ -117,7 +119,6 @@ async function installCodingFirstChatHost(
|
|
|
|
|
initialized: true,
|
|
|
|
|
agents: [agent],
|
|
|
|
|
knowledgeDirectory: 'knowledge',
|
|
|
|
|
legacyConversationNotice: featureComplete ? 'pending' : 'none',
|
|
|
|
|
createdAt: now,
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
};
|
|
|
|
|
@@ -165,8 +166,20 @@ async function installCodingFirstChatHost(
|
|
|
|
|
id: 'message-assistant-e2e',
|
|
|
|
|
sourceEntryId: 'entry-assistant-e2e',
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
status: 'complete',
|
|
|
|
|
blocks: [{ kind: 'text', id: 'message-assistant-e2e:content:0', text: 'Durable assistant response', status: 'complete' }],
|
|
|
|
|
status: 'streaming',
|
|
|
|
|
blocks: [
|
|
|
|
|
{
|
|
|
|
|
kind: 'thinking',
|
|
|
|
|
id: 'message-assistant-e2e:thinking:0',
|
|
|
|
|
text: [
|
|
|
|
|
'Inspecting the project before responding.',
|
|
|
|
|
'Checking the runtime boundary and current configuration.',
|
|
|
|
|
'Preparing the smallest safe next step.',
|
|
|
|
|
].join('\n\n'),
|
|
|
|
|
status: 'streaming',
|
|
|
|
|
},
|
|
|
|
|
{ kind: 'text', id: 'message-assistant-e2e:content:0', text: 'Durable assistant response', status: 'complete' },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
kind: 'subagent',
|
|
|
|
|
@@ -199,9 +212,25 @@ async function installCodingFirstChatHost(
|
|
|
|
|
title: '浏览器状态',
|
|
|
|
|
inputText: 'status',
|
|
|
|
|
status: 'complete',
|
|
|
|
|
output: [{ kind: 'text', id: 'browser-output-e2e', text: 'Browser ready', status: 'complete' }],
|
|
|
|
|
output: [{
|
|
|
|
|
kind: 'text',
|
|
|
|
|
id: 'browser-output-e2e',
|
|
|
|
|
text: `${'Checking browser runtime state and the latest navigation checkpoint. '.repeat(5)}Browser ready`,
|
|
|
|
|
status: 'complete',
|
|
|
|
|
}],
|
|
|
|
|
details: { schema: 'agent-browser.v1', action: 'status' },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
kind: 'tool',
|
|
|
|
|
id: 'changes-tool-e2e',
|
|
|
|
|
toolCallId: 'changes-call-e2e',
|
|
|
|
|
toolName: 'changed_file',
|
|
|
|
|
title: '记录文件更改',
|
|
|
|
|
inputText: 'src/app.ts',
|
|
|
|
|
status: 'complete',
|
|
|
|
|
output: [],
|
|
|
|
|
details: { schema: 'changed-file.v1', paths: ['src/app.ts'] },
|
|
|
|
|
},
|
|
|
|
|
] : [],
|
|
|
|
|
run: featureComplete ? { status: 'running', runId: 'run-e2e-feature', mode: 'prompt' } : { status: 'idle' },
|
|
|
|
|
queue: { items: featureComplete ? [{ id: 'queue-e2e', clientRequestId: 'request-queued', mode: 'follow-up', text: 'Queued follow-up', attachmentIds: [] }] : [] },
|
|
|
|
|
@@ -212,9 +241,13 @@ async function installCodingFirstChatHost(
|
|
|
|
|
id: 'interaction-e2e',
|
|
|
|
|
conversationId: conversation.id,
|
|
|
|
|
runId: 'run-e2e-feature',
|
|
|
|
|
kind: 'confirm',
|
|
|
|
|
kind: 'select',
|
|
|
|
|
title: '允许继续?',
|
|
|
|
|
message: '确认当前实现方向。',
|
|
|
|
|
options: [
|
|
|
|
|
{ id: 'interaction-e2e:option:0', label: '按当前方向继续' },
|
|
|
|
|
{ id: 'interaction-e2e:option:1', label: '重新调整方向' },
|
|
|
|
|
],
|
|
|
|
|
status: 'pending',
|
|
|
|
|
}] : [],
|
|
|
|
|
worker: { status: 'ready', generation: 1 },
|
|
|
|
|
@@ -229,10 +262,89 @@ async function installCodingFirstChatHost(
|
|
|
|
|
model: {
|
|
|
|
|
model: secondConversation.model,
|
|
|
|
|
modelResolution: secondConversation.modelResolution,
|
|
|
|
|
...(featureComplete
|
|
|
|
|
? { availableThinkingLevels: ['off', 'low', 'medium', 'high'] }
|
|
|
|
|
: {}),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
nodes: [],
|
|
|
|
|
run: { status: 'idle' },
|
|
|
|
|
nodes: featureComplete ? [
|
|
|
|
|
{
|
|
|
|
|
kind: 'message',
|
|
|
|
|
id: 'message-second-user-e2e',
|
|
|
|
|
role: 'user',
|
|
|
|
|
status: 'complete',
|
|
|
|
|
blocks: [{ kind: 'text', id: 'message-second-user-e2e:content:0', text: '检查第二个项目', status: 'complete' }],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
kind: 'boundary',
|
|
|
|
|
id: 'boundary-second-start-e2e',
|
|
|
|
|
runId: 'run-second-e2e',
|
|
|
|
|
boundary: 'turn-start',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
kind: 'message',
|
|
|
|
|
id: 'message-second-work-e2e',
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
status: 'complete',
|
|
|
|
|
stopReason: 'tool-use',
|
|
|
|
|
blocks: [
|
|
|
|
|
{ kind: 'thinking', id: 'message-second-work-e2e:thinking:0', text: '读取配置并核对入口。', status: 'complete' },
|
|
|
|
|
{ kind: 'text', id: 'message-second-work-e2e:content:0', text: '我先读取配置。', status: 'complete' },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
kind: 'tool',
|
|
|
|
|
id: 'tool-second-e2e',
|
|
|
|
|
toolCallId: 'tool-call-second-e2e',
|
|
|
|
|
toolName: 'agent_browser',
|
|
|
|
|
title: '检查网站',
|
|
|
|
|
inputText: 'status',
|
|
|
|
|
status: 'error',
|
|
|
|
|
output: [{ kind: 'text', id: 'tool-second-e2e:output:0', text: 'Bridge request failed', status: 'complete' }],
|
|
|
|
|
details: { schema: 'agent-browser.v1', action: 'status' },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
kind: 'boundary',
|
|
|
|
|
id: 'boundary-second-end-e2e',
|
|
|
|
|
runId: 'run-second-e2e',
|
|
|
|
|
boundary: 'turn-end',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
kind: 'message',
|
|
|
|
|
id: 'message-second-final-e2e',
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
status: 'complete',
|
|
|
|
|
stopReason: 'stop',
|
|
|
|
|
blocks: [{
|
|
|
|
|
kind: 'text',
|
|
|
|
|
id: 'message-second-final-e2e:content:0',
|
|
|
|
|
text: [
|
|
|
|
|
'## 检查结论',
|
|
|
|
|
'',
|
|
|
|
|
'结论:第二个项目配置正常。',
|
|
|
|
|
'',
|
|
|
|
|
'文件位置:`~/Desktop/123/guiyang/index.html`',
|
|
|
|
|
'',
|
|
|
|
|
'查看方式:打开 `guiyang/index.html`,或访问 `http://localhost:8642/index.html`。',
|
|
|
|
|
'',
|
|
|
|
|
'| 检查项 | 状态 |',
|
|
|
|
|
'| --- | --- |',
|
|
|
|
|
'| 项目配置 | 正常 |',
|
|
|
|
|
'',
|
|
|
|
|
'[查看项目文档](https://example.com/docs)',
|
|
|
|
|
].join('\n'),
|
|
|
|
|
status: 'complete',
|
|
|
|
|
}],
|
|
|
|
|
},
|
|
|
|
|
] : [],
|
|
|
|
|
run: featureComplete ? {
|
|
|
|
|
status: 'idle',
|
|
|
|
|
runId: 'run-second-e2e',
|
|
|
|
|
mode: 'prompt',
|
|
|
|
|
startedAt: 1_000,
|
|
|
|
|
settledAt: 5_000,
|
|
|
|
|
terminalReason: 'completed',
|
|
|
|
|
} : { status: 'idle' },
|
|
|
|
|
queue: { items: [] },
|
|
|
|
|
pendingInteractions: [],
|
|
|
|
|
worker: {
|
|
|
|
|
@@ -338,16 +450,6 @@ async function installCodingFirstChatHost(
|
|
|
|
|
if (path === `/api/coding/projects/config?projectId=${project.id}`) {
|
|
|
|
|
return respond({ snapshot: { project, config, knowledgeFiles: [] } });
|
|
|
|
|
}
|
|
|
|
|
if (path === '/api/coding/projects/legacy-conversation-notice/acknowledge'
|
|
|
|
|
&& method === 'POST') {
|
|
|
|
|
return respond({
|
|
|
|
|
snapshot: {
|
|
|
|
|
project,
|
|
|
|
|
config: { ...config, legacyConversationNotice: 'acknowledged' },
|
|
|
|
|
knowledgeFiles: [],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (path === `/api/coding/projects/conversations?projectId=${project.id}`) {
|
|
|
|
|
return respond({
|
|
|
|
|
conversations: featureComplete
|
|
|
|
|
@@ -367,7 +469,11 @@ async function installCodingFirstChatHost(
|
|
|
|
|
await new Promise<void>((resolve) => { state.releaseSnapshot = resolve; });
|
|
|
|
|
state.snapshotPending = false;
|
|
|
|
|
}
|
|
|
|
|
return respond({ snapshot });
|
|
|
|
|
return respond({
|
|
|
|
|
snapshot: state.interactionAnswered
|
|
|
|
|
? { ...snapshot, pendingInteractions: [] }
|
|
|
|
|
: snapshot,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (path === `/api/coding/conversations/${secondConversation.id}/snapshot`) {
|
|
|
|
|
return respond({ snapshot: secondSnapshot });
|
|
|
|
|
@@ -399,6 +505,7 @@ async function installCodingFirstChatHost(
|
|
|
|
|
return respond({ conversation: { ...conversation, ...(body?.title ? { title: body.title } : {}), unread: body?.unread === true, archivedAt: body?.archived === true ? now : null } });
|
|
|
|
|
}
|
|
|
|
|
if (/^\/api\/coding\/interactions\/[^/]+\/respond$/.test(path) && method === 'POST') {
|
|
|
|
|
state.interactionAnswered = true;
|
|
|
|
|
return respond({});
|
|
|
|
|
}
|
|
|
|
|
if (/^\/api\/coding\/conversations\/[^/]+\/changes$/.test(path)) {
|
|
|
|
|
@@ -536,7 +643,7 @@ test('first PI Conversation is editable under 500 ms and submits before runtime
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('PI feature UI isolates Conversations and exposes queue, interaction, model, subagent, and project tools', async ({
|
|
|
|
|
test('PI feature UI isolates Conversations and exposes queue, interaction, model, and subagent state', async ({
|
|
|
|
|
launchElectronApp,
|
|
|
|
|
}) => {
|
|
|
|
|
const electronApp = await launchElectronApp({ skipSetup: true });
|
|
|
|
|
@@ -555,31 +662,139 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
|
|
|
|
await expect(page.getByTestId('main-layout')).toBeVisible();
|
|
|
|
|
await page.evaluate(() => { window.location.hash = '/chat'; });
|
|
|
|
|
|
|
|
|
|
await expect(page.getByTestId('coding-conversation-header')).toContainText('生成中');
|
|
|
|
|
await expect(page.getByTestId('coding-conversation-header')).toContainText('队列 1');
|
|
|
|
|
await expect(page.getByText('并行子任务')).toBeVisible();
|
|
|
|
|
await expect(page.getByTestId('coding-conversation-sidebar-titlebar')).toBeVisible();
|
|
|
|
|
await expect(page.getByTestId('coding-conversation-header-titlebar')).toBeVisible();
|
|
|
|
|
const titlebarBounds = await Promise.all([
|
|
|
|
|
page.getByTestId('coding-conversation-sidebar-titlebar').boundingBox(),
|
|
|
|
|
page.getByTestId('coding-conversation-header-titlebar').boundingBox(),
|
|
|
|
|
]);
|
|
|
|
|
expect(titlebarBounds.every((bounds) => bounds?.y === 0 && bounds.height === 40)).toBe(true);
|
|
|
|
|
const logoBounds = await page.getByTestId('titlebar-logo').boundingBox();
|
|
|
|
|
const windowWidth = await page.evaluate(() => window.innerWidth);
|
|
|
|
|
expect(logoBounds).not.toBeNull();
|
|
|
|
|
expect(Math.abs((logoBounds!.x + logoBounds!.width) - windowWidth)).toBeLessThanOrEqual(1);
|
|
|
|
|
const conversationHeader = page.getByTestId('coding-conversation-header');
|
|
|
|
|
await expect(conversationHeader).toContainText('新对话');
|
|
|
|
|
await expect(conversationHeader).not.toContainText('Pi ·');
|
|
|
|
|
await expect(conversationHeader).not.toContainText('队列 1');
|
|
|
|
|
await expect(page.getByTestId('coding-conversation-sidebar-titlebar')).not.toContainText('Pi 本地对话');
|
|
|
|
|
await expect(conversationHeader.getByRole('button', { name: '创建分支' })).toHaveCount(0);
|
|
|
|
|
await expect(conversationHeader.getByRole('button', { name: /标为(已读|未读)/ })).toHaveCount(0);
|
|
|
|
|
await expect(conversationHeader.getByRole('button', { name: '归档' })).toHaveCount(0);
|
|
|
|
|
await expect(conversationHeader.getByRole('button', { name: '智能体设置' })).toHaveCount(0);
|
|
|
|
|
await expect(conversationHeader.getByRole('button', { name: '打开编程工具' })).toHaveCount(0);
|
|
|
|
|
await expect(page.getByRole('button', { name: '项目设置' })).toHaveCount(1);
|
|
|
|
|
await expect(page.getByTestId('coding-conversation-sidebar')).toBeVisible();
|
|
|
|
|
const activeProcess = page.getByTestId('coding-process-group');
|
|
|
|
|
await expect(activeProcess).toHaveAttribute('open', '');
|
|
|
|
|
await expect(activeProcess.locator('summary').first()).toContainText('处理中');
|
|
|
|
|
const activeThinking = page.getByLabel('思考过程');
|
|
|
|
|
await expect(activeThinking).toContainText('Preparing the smallest safe next step.');
|
|
|
|
|
await expect(activeThinking).not.toContainText('Inspecting the project before responding.');
|
|
|
|
|
await expect(activeThinking).toHaveAttribute('data-collapsed-lines', '1');
|
|
|
|
|
await expect(activeThinking).toHaveAttribute('data-expanded', 'false');
|
|
|
|
|
await expect(activeThinking).toHaveAttribute('data-streaming', 'true');
|
|
|
|
|
const activeThinkingPreview = activeThinking.getByTestId('process-progress-preview');
|
|
|
|
|
await expect(activeThinkingPreview).toHaveAttribute('data-progress-tail', 'true');
|
|
|
|
|
await expect(activeThinkingPreview).toHaveAttribute('data-progress-alignment', 'left');
|
|
|
|
|
await expect(activeThinkingPreview).toHaveAttribute('data-progress-update-motion', 'none');
|
|
|
|
|
await expect(activeThinkingPreview).not.toHaveAttribute('data-roll-revision');
|
|
|
|
|
await expect(activeThinkingPreview.locator('.streaming-progress-roll')).toHaveCount(0);
|
|
|
|
|
await expect(activeThinkingPreview).toHaveAttribute('data-progress-shimmer', 'true');
|
|
|
|
|
await expect(activeThinkingPreview).toHaveCSS('text-align', 'left');
|
|
|
|
|
const activeShimmer = activeThinkingPreview.locator('.streaming-progress-shimmer');
|
|
|
|
|
await expect(activeShimmer).toBeVisible();
|
|
|
|
|
expect(await activeShimmer.evaluate((element) => (
|
|
|
|
|
getComputedStyle(element).animationName
|
|
|
|
|
))).toContain('makelore-progress-shimmer');
|
|
|
|
|
const collapsedThinkingBounds = await activeThinking.boundingBox();
|
|
|
|
|
expect(collapsedThinkingBounds).not.toBeNull();
|
|
|
|
|
expect(collapsedThinkingBounds!.height).toBeLessThanOrEqual(24);
|
|
|
|
|
const thinkingToggle = page.getByRole('button', { name: '展开思考详情' });
|
|
|
|
|
await expect(thinkingToggle).toHaveAttribute('aria-expanded', 'false');
|
|
|
|
|
await thinkingToggle.click();
|
|
|
|
|
await expect(activeThinking).toHaveAttribute('data-expanded', 'true');
|
|
|
|
|
await expect(activeThinking).toContainText('Inspecting the project before responding.');
|
|
|
|
|
await expect(activeThinking.getByTestId('assistant-markdown'))
|
|
|
|
|
.toHaveClass(/text-muted-foreground/);
|
|
|
|
|
await expect.poll(async () => (
|
|
|
|
|
(await activeThinking.boundingBox())?.height ?? 0
|
|
|
|
|
)).toBeGreaterThan(collapsedThinkingBounds!.height);
|
|
|
|
|
await page.getByRole('button', { name: '收起思考详情' }).click();
|
|
|
|
|
await expect(activeThinking).toHaveAttribute('data-expanded', 'false');
|
|
|
|
|
const activeCommentary = page.locator('[data-node-kind="assistant-commentary"]');
|
|
|
|
|
await expect(activeCommentary).toHaveCount(1);
|
|
|
|
|
await expect(activeCommentary).toHaveClass(/text-foreground/);
|
|
|
|
|
const activeCommentaryPreview = activeCommentary.getByTestId('process-progress-preview');
|
|
|
|
|
await expect(activeCommentaryPreview).toContainText('Durable assistant response');
|
|
|
|
|
await expect(activeCommentaryPreview).toHaveClass(/text-foreground/);
|
|
|
|
|
await expect(activeCommentaryPreview).not.toHaveClass(/text-muted-foreground/);
|
|
|
|
|
await expect(activeCommentaryPreview).toHaveAttribute('data-progress-shimmer', 'false');
|
|
|
|
|
await expect(activeCommentaryPreview).toHaveAttribute('data-progress-update-motion', 'none');
|
|
|
|
|
await expect(activeCommentaryPreview.locator('.streaming-progress-shimmer')).toHaveCount(0);
|
|
|
|
|
await expect(activeCommentaryPreview.locator('.streaming-progress-roll')).toHaveCount(0);
|
|
|
|
|
await page.getByRole('button', { name: '展开过程说明' }).click();
|
|
|
|
|
await expect(activeCommentary.getByTestId('assistant-markdown')).toHaveClass(/text-foreground/);
|
|
|
|
|
await expect(activeCommentary.getByTestId('assistant-markdown'))
|
|
|
|
|
.not.toHaveClass(/text-muted-foreground/);
|
|
|
|
|
await page.getByRole('button', { name: '收起过程说明' }).click();
|
|
|
|
|
await expect(page.locator('[data-node-id="subagent-e2e"] > summary')).toContainText('并行子任务');
|
|
|
|
|
await page.locator('[data-node-id="compaction-e2e"] > summary').click();
|
|
|
|
|
await expect(page.getByText('系统会自动重试。')).toBeVisible();
|
|
|
|
|
await page.locator('[data-node-id="browser-tool-e2e"] > summary').click();
|
|
|
|
|
const compactTool = page.locator('[data-node-id="browser-tool-e2e"]');
|
|
|
|
|
const compactToolSummary = compactTool.locator('> summary');
|
|
|
|
|
await expect(compactTool).not.toHaveAttribute('open', '');
|
|
|
|
|
await expect(compactToolSummary).toHaveAttribute('data-collapsed-lines', '1');
|
|
|
|
|
await expect(compactTool.getByTestId('tool-progress-preview')).toContainText('Browser ready');
|
|
|
|
|
await expect(compactTool.getByTestId('tool-progress-preview'))
|
|
|
|
|
.toHaveAttribute('data-progress-tail', 'true');
|
|
|
|
|
const toolProgressViewport = compactTool.getByTestId('tool-progress-preview');
|
|
|
|
|
await expect(toolProgressViewport).toHaveAttribute('data-progress-alignment', 'left');
|
|
|
|
|
await expect(toolProgressViewport).toHaveAttribute('data-progress-shimmer', 'false');
|
|
|
|
|
await expect(toolProgressViewport.locator('.streaming-progress-shimmer')).toHaveCount(0);
|
|
|
|
|
await expect(toolProgressViewport).toHaveCSS('text-align', 'left');
|
|
|
|
|
const toolProgressScroll = await toolProgressViewport.evaluate((element) => ({
|
|
|
|
|
clientWidth: element.clientWidth,
|
|
|
|
|
scrollLeft: element.scrollLeft,
|
|
|
|
|
scrollWidth: element.scrollWidth,
|
|
|
|
|
}));
|
|
|
|
|
expect(toolProgressScroll.scrollWidth).toBeGreaterThan(toolProgressScroll.clientWidth);
|
|
|
|
|
expect(Math.abs(
|
|
|
|
|
toolProgressScroll.scrollLeft
|
|
|
|
|
- (toolProgressScroll.scrollWidth - toolProgressScroll.clientWidth),
|
|
|
|
|
)).toBeLessThanOrEqual(2);
|
|
|
|
|
const compactToolBounds = await compactToolSummary.boundingBox();
|
|
|
|
|
expect(compactToolBounds).not.toBeNull();
|
|
|
|
|
expect(compactToolBounds!.height).toBeLessThanOrEqual(33);
|
|
|
|
|
await compactToolSummary.click();
|
|
|
|
|
await expect(page.getByText('浏览器操作 · status')).toBeVisible();
|
|
|
|
|
await expect(page.getByText('允许继续?')).toBeVisible();
|
|
|
|
|
await expect(page.getByTestId('legacy-conversation-notice')).toBeVisible();
|
|
|
|
|
await page.getByTestId('legacy-conversation-notice').getByRole('button', { name: '知道了' }).click();
|
|
|
|
|
const changesSummary = page.getByRole('button', { name: '查看 1 个文件的更改' });
|
|
|
|
|
await expect(changesSummary).toContainText('1 个文件已更改');
|
|
|
|
|
await expect(changesSummary).toContainText('+1');
|
|
|
|
|
await changesSummary.click();
|
|
|
|
|
await expect(page.getByText('src/app.ts')).toBeVisible();
|
|
|
|
|
await expect(page.getByTestId('legacy-conversation-notice')).toHaveCount(0);
|
|
|
|
|
await page.getByRole('button', { name: '确认', exact: true }).click();
|
|
|
|
|
const interactionPanel = page.getByTestId('coding-interactions');
|
|
|
|
|
await interactionPanel.getByRole('textbox', { name: '允许继续?的其他回答' })
|
|
|
|
|
.fill('先补充错误处理再继续');
|
|
|
|
|
await interactionPanel.getByRole('button', { name: '提交回答' }).click();
|
|
|
|
|
await expect(interactionPanel).toHaveCount(0);
|
|
|
|
|
|
|
|
|
|
await page.getByTestId('coding-message-composer').getByRole('textbox').fill('排到下一轮验证');
|
|
|
|
|
const mode = page.getByRole('combobox', { name: '消息发送方式' });
|
|
|
|
|
await mode.selectOption('follow-up');
|
|
|
|
|
await expect(mode).toHaveValue('follow-up');
|
|
|
|
|
|
|
|
|
|
const model = page.getByRole('combobox', { name: '当前对话模型' });
|
|
|
|
|
await model.selectOption(JSON.stringify(['account-e2e', 'model-b']));
|
|
|
|
|
await page.getByRole('button', { name: '中止' }).click();
|
|
|
|
|
|
|
|
|
|
await page.getByRole('button', { name: '打开编程工具' }).click();
|
|
|
|
|
await expect(page.getByRole('dialog')).toContainText('编程工具');
|
|
|
|
|
await page.getByRole('tab', { name: '命令' }).click();
|
|
|
|
|
await page.getByRole('button', { name: /\/review/ }).click();
|
|
|
|
|
await expect(page.getByRole('textbox')).toHaveValue('/review ');
|
|
|
|
|
const composer = page.getByTestId('coding-message-composer');
|
|
|
|
|
await expect(composer).not.toContainText('项目内可写');
|
|
|
|
|
await expect(composer.getByRole('button', { name: '整理上下文' })).toHaveCount(0);
|
|
|
|
|
await expect(composer.getByRole('button', { name: '语音输入' })).toBeVisible();
|
|
|
|
|
const runtimeSettings = composer.getByRole('button', { name: /模型与思考设置/ });
|
|
|
|
|
await expect(runtimeSettings).toHaveClass(/bg-transparent/);
|
|
|
|
|
await expect(runtimeSettings).not.toHaveClass(/bg-surface-subtle\/75/);
|
|
|
|
|
await expect(runtimeSettings).toBeDisabled();
|
|
|
|
|
await page.getByRole('button', { name: '中止', exact: true }).click();
|
|
|
|
|
|
|
|
|
|
const builderConversations = page.getByRole('group', { name: 'Builder 的对话' });
|
|
|
|
|
await expect(builderConversations).toBeVisible();
|
|
|
|
|
@@ -595,32 +810,105 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
|
|
|
|
await expect(page.getByRole('button', { name: '从这里创建新对话分支' })).toHaveCount(1);
|
|
|
|
|
await builderConversations.getByRole('button', { name: 'Second Conversation' }).click();
|
|
|
|
|
await expect(page.getByTestId('coding-conversation-header')).toContainText('Second Conversation');
|
|
|
|
|
await expect(page.getByRole('combobox', { name: '当前对话模型' })).toHaveValue(
|
|
|
|
|
JSON.stringify(['account-e2e', 'model-b']),
|
|
|
|
|
const completedProcess = page.getByTestId('coding-process-group');
|
|
|
|
|
await expect(completedProcess).not.toHaveAttribute('open', '');
|
|
|
|
|
await expect(completedProcess.locator('summary').first()).toContainText('已处理 4 秒');
|
|
|
|
|
await expect(completedProcess.locator('summary').first()).not.toContainText('处理失败');
|
|
|
|
|
await expect(page.getByRole('heading', { level: 2, name: '检查结论' })).toBeVisible();
|
|
|
|
|
await expect(page.getByText('结论:第二个项目配置正常。')).toBeVisible();
|
|
|
|
|
await expect(page.getByRole('table')).toContainText('项目配置');
|
|
|
|
|
await expect(page.getByRole('link', { name: '查看项目文档' }))
|
|
|
|
|
.toHaveAttribute('href', 'https://example.com/docs');
|
|
|
|
|
const localReplyLink = page.getByRole('button', {
|
|
|
|
|
name: '用默认浏览器打开 guiyang/index.html',
|
|
|
|
|
});
|
|
|
|
|
await expect(localReplyLink).toBeVisible();
|
|
|
|
|
await expect(localReplyLink).toHaveClass(/text-brand/);
|
|
|
|
|
await expect(localReplyLink).not.toHaveClass(/bg-/);
|
|
|
|
|
const localhostReplyLink = page.getByRole('link', {
|
|
|
|
|
name: 'http://localhost:8642/index.html',
|
|
|
|
|
});
|
|
|
|
|
await expect(localhostReplyLink)
|
|
|
|
|
.toHaveAttribute('href', 'http://localhost:8642/index.html');
|
|
|
|
|
await expect(localhostReplyLink).toHaveClass(/text-brand/);
|
|
|
|
|
await expect(localhostReplyLink).not.toHaveClass(/bg-/);
|
|
|
|
|
await expect(page.getByText('开始新一轮')).toHaveCount(0);
|
|
|
|
|
await expect(page.getByText('本轮已结算')).toHaveCount(0);
|
|
|
|
|
await completedProcess.locator('summary').first().click();
|
|
|
|
|
await expect(page.getByLabel('思考过程')).toContainText('读取配置并核对入口。');
|
|
|
|
|
const completedCommentary = page.locator(
|
|
|
|
|
'[data-node-id="message-second-work-e2e"][data-node-kind="assistant-commentary"]',
|
|
|
|
|
);
|
|
|
|
|
await expect(completedCommentary.getByTestId('process-progress-preview'))
|
|
|
|
|
.toContainText('我先读取配置。');
|
|
|
|
|
await expect(completedCommentary.getByTestId('process-progress-preview'))
|
|
|
|
|
.toHaveClass(/text-foreground/);
|
|
|
|
|
await expect(completedCommentary.getByTestId('process-progress-preview'))
|
|
|
|
|
.not.toHaveClass(/text-muted-foreground/);
|
|
|
|
|
await expect(page.locator('[data-node-id="tool-second-e2e"] > summary')).toContainText('失败');
|
|
|
|
|
const secondRuntimeSettings = composer.getByRole('button', { name: /模型与思考设置/ });
|
|
|
|
|
await expect(secondRuntimeSettings).toBeEnabled();
|
|
|
|
|
await expect(secondRuntimeSettings).toContainText('model-b');
|
|
|
|
|
await secondRuntimeSettings.click();
|
|
|
|
|
await page.getByRole('menuitem', { name: '推理强度 低' }).click();
|
|
|
|
|
await expect(page.getByRole('menuitemradio', { name: '关闭' })).toBeVisible();
|
|
|
|
|
await expect(page.getByRole('menuitemradio', { name: '低' })).toBeVisible();
|
|
|
|
|
await expect(page.getByRole('menuitemradio', { name: '中等' })).toBeVisible();
|
|
|
|
|
await expect(page.getByRole('menuitemradio', { name: '高' })).toBeVisible();
|
|
|
|
|
await expect(page.getByRole('menuitemradio', { name: '极简' })).toHaveCount(0);
|
|
|
|
|
await page.keyboard.press('Escape');
|
|
|
|
|
await page.keyboard.press('Escape');
|
|
|
|
|
await page.evaluate(() => {
|
|
|
|
|
const trackedWindow = window as typeof window & {
|
|
|
|
|
__makeloreReconnectObserver?: MutationObserver;
|
|
|
|
|
__makeloreSawReconnectBanner?: boolean;
|
|
|
|
|
};
|
|
|
|
|
trackedWindow.__makeloreSawReconnectBanner = false;
|
|
|
|
|
trackedWindow.__makeloreReconnectObserver = new MutationObserver(() => {
|
|
|
|
|
if (document.body.textContent?.includes('正在重新连接本地 Agent')) {
|
|
|
|
|
trackedWindow.__makeloreSawReconnectBanner = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
trackedWindow.__makeloreReconnectObserver.observe(document.body, {
|
|
|
|
|
childList: true,
|
|
|
|
|
subtree: true,
|
|
|
|
|
characterData: true,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
await secondRuntimeSettings.click();
|
|
|
|
|
await page.getByRole('menuitem', { name: '模型 model-b' }).click();
|
|
|
|
|
await page.getByRole('menuitemradio', { name: 'model-a' }).click();
|
|
|
|
|
await expect(secondRuntimeSettings).toContainText('model-b');
|
|
|
|
|
expect(await page.evaluate(() => {
|
|
|
|
|
const trackedWindow = window as typeof window & {
|
|
|
|
|
__makeloreReconnectObserver?: MutationObserver;
|
|
|
|
|
__makeloreSawReconnectBanner?: boolean;
|
|
|
|
|
};
|
|
|
|
|
trackedWindow.__makeloreReconnectObserver?.disconnect();
|
|
|
|
|
return trackedWindow.__makeloreSawReconnectBanner ?? false;
|
|
|
|
|
})).toBe(false);
|
|
|
|
|
await expect(page.getByRole('textbox')).toHaveValue('');
|
|
|
|
|
await page.getByRole('button', { name: '恢复' }).click();
|
|
|
|
|
await page.getByRole('button', { name: '整理上下文' }).click();
|
|
|
|
|
|
|
|
|
|
await page.getByRole('button', { name: '打开编程工具' }).click();
|
|
|
|
|
await page.getByRole('tab', { name: '技能' }).click();
|
|
|
|
|
await expect(page.getByRole('dialog')).toContainText('Research');
|
|
|
|
|
|
|
|
|
|
await expect(page.getByText(/分享|取消分享|回滚|恢复回滚|待办|全局运行时/)).toHaveCount(0);
|
|
|
|
|
await expect(page.getByText(/编程工具|分享|取消分享|回滚|恢复回滚|待办|全局运行时/)).toHaveCount(0);
|
|
|
|
|
const state = await readState(electronApp);
|
|
|
|
|
expect(state.captured.some((request) => request.path.endsWith('/model') && request.method === 'POST')).toBe(true);
|
|
|
|
|
expect(state.captured.some((request) => request.path.endsWith('/abort') && request.method === 'POST')).toBe(true);
|
|
|
|
|
expect(state.captured.some((request) => request.path.includes('/interactions/') && request.path.endsWith('/respond'))).toBe(true);
|
|
|
|
|
expect(state.captured.some((request) => request.path.endsWith('/commands'))).toBe(true);
|
|
|
|
|
expect(state.captured.some((request) => (
|
|
|
|
|
request.path.includes('/interactions/')
|
|
|
|
|
&& request.path.endsWith('/respond')
|
|
|
|
|
&& request.body?.value === '先补充错误处理再继续'
|
|
|
|
|
))).toBe(true);
|
|
|
|
|
expect(state.captured.some((request) => request.path.endsWith('/commands'))).toBe(false);
|
|
|
|
|
expect(state.captured.some((request) => request.path.endsWith('/changes'))).toBe(true);
|
|
|
|
|
expect(state.captured.some((request) => request.path.endsWith('/recover') && request.method === 'POST')).toBe(true);
|
|
|
|
|
expect(state.captured.some((request) => request.path.endsWith('/compact') && request.method === 'POST')).toBe(true);
|
|
|
|
|
expect(state.captured.some((request) => request.path.endsWith('/compact') && request.method === 'POST')).toBe(false);
|
|
|
|
|
expect(state.captured.some((request) => (
|
|
|
|
|
request.path === '/api/coding/conversations/conversation-pi-first-chat/fork'
|
|
|
|
|
&& request.method === 'POST'
|
|
|
|
|
&& request.body?.sourceEntryId === 'entry-user-e2e'
|
|
|
|
|
))).toBe(true);
|
|
|
|
|
expect(state.captured.some((request) => request.path.includes('legacy-conversation-notice'))).toBe(true);
|
|
|
|
|
expect(state.captured.some((request) => request.path.includes('legacy-conversation-notice'))).toBe(false);
|
|
|
|
|
expect(state.captured.every((request) => !request.path.includes('/api/opencode/share'))).toBe(true);
|
|
|
|
|
} finally {
|
|
|
|
|
await releaseSnapshot(electronApp);
|
|
|
|
|
|