Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 83 additions & 7 deletions apps/desktop/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export async function waitForInvocableSkills(
* backend (BackendRegistry override in main); this only satisfies the UI
* readiness gates. Kept in the fixture so test data stays out of production main.
*/
async function seedE2eConnection(userDataDir: string): Promise<void> {
async function seedE2eConnection(userDataDir: string): Promise<string> {
const workspaceRoot = path.join(userDataDir, 'workspaces', 'default');
const capability = await resolveStorageRoot({ path: workspaceRoot, kind: 'interactive' });
const owner = await tryAcquireInteractiveRootOwner(capability);
Expand Down Expand Up @@ -197,6 +197,7 @@ async function seedE2eConnection(userDataDir: string): Promise<void> {
if (defaultTarget.kind !== 'committed') {
throw new Error(`E2E default target seed was not committed: ${defaultTarget.kind}`);
}
return connection.connectionId;
} finally {
await owner.close();
}
Expand Down Expand Up @@ -231,20 +232,28 @@ async function seedRailRenderSessions(userDataDir: string): Promise<void> {
}
}

async function seedParentRemovalSessions(userDataDir: string): Promise<void> {
async function seedParentRemovalSessions(
userDataDir: string,
connectionId: string,
): Promise<void> {
const workspaceRoot = path.join(userDataDir, 'workspaces', 'default');
const projectRoot = path.join(userDataDir, 'project');
const now = Date.UTC(2026, 4, 22, 3, 0, 0);
await mkdir(projectRoot, { recursive: true });
const store = createSessionStore(workspaceRoot);
try {
const parent = await store.create({
cwd: path.join(userDataDir, 'project'),
cwd: projectRoot,
llmConnectionId: connectionId,
llmConnectionSlug: 'e2e',
model: 'claude-sonnet-4-5-20250929',
permissionMode: 'ask',
name: PARENT_REMOVAL_PARENT_NAME,
labels: [],
});
await store.createSubagent({
cwd: path.join(userDataDir, 'project'),
const child = await store.createSubagent({
cwd: projectRoot,
llmConnectionId: connectionId,
llmConnectionSlug: 'e2e',
model: 'claude-sonnet-4-5-20250929',
permissionMode: 'ask',
Expand Down Expand Up @@ -277,6 +286,70 @@ async function seedParentRemovalSessions(userDataDir: string): Promise<void> {
initialRunId: 'e2e-child-run',
},
});
await store.appendMessages(parent.id, [
{
type: 'user',
id: 'e2e-parent-user',
turnId: 'e2e-parent-turn',
ts: now - 2_000,
text: '请让实现子任务检查侧边对话的保留行为。',
},
{
type: 'tool_call',
id: 'e2e-spawn-call',
turnId: 'e2e-parent-turn',
ts: now - 1_900,
toolName: 'spawn_subagent',
displayName: 'Implementation',
intent: '检查侧边对话在父子任务间的连续性',
args: {},
},
{
type: 'tool_result',
id: 'e2e-spawn-result',
turnId: 'e2e-parent-turn',
ts: now - 1_800,
toolUseId: 'e2e-spawn-call',
isError: false,
content: {
kind: 'subagent',
childSessionId: child.header.id,
agentId: 'implementation',
agentName: 'Implementation',
turnId: 'e2e-child-turn',
runId: 'e2e-child-run',
status: 'completed',
permissionMode: 'ask',
summary: '已完成侧边对话连续性检查',
artifactIds: [],
},
},
{
type: 'assistant',
id: 'e2e-parent-assistant',
turnId: 'e2e-parent-turn',
ts: now - 1_700,
text: '实现子任务已完成检查。',
modelId: 'claude-sonnet-4-5-20250929',
},
]);
await store.appendMessages(child.header.id, [
{
type: 'user',
id: 'e2e-child-user',
turnId: 'e2e-child-turn',
ts: now - 1_600,
text: '检查父任务中打开的侧边对话。',
},
{
type: 'assistant',
id: 'e2e-child-assistant',
turnId: 'e2e-child-turn',
ts: now - 1_500,
text: '已确认切换到子任务后,父任务的侧边对话应继续保留。',
modelId: 'claude-sonnet-4-5-20250929',
},
]);
} finally {
await store.close?.();
}
Expand Down Expand Up @@ -450,8 +523,11 @@ async function withE2eWindow(
const mainLogs: string[] = [];
const rendererLogs: string[] = [];
try {
if (seed) await seedE2eConnection(userDataDir);
if (parentRemovalSessions) await seedParentRemovalSessions(userDataDir);
const e2eConnectionId = seed ? await seedE2eConnection(userDataDir) : undefined;
if (parentRemovalSessions) {
if (!e2eConnectionId) throw new Error('Parent-removal fixture requires a seeded connection');
await seedParentRemovalSessions(userDataDir, e2eConnectionId);
}
if (railRenderSessions) await seedRailRenderSessions(userDataDir);
if (invocableSkills) await seedE2eInvocableSkills(userDataDir);
if (gitReviewExtraFiles !== undefined) {
Expand Down
44 changes: 43 additions & 1 deletion apps/desktop/e2e/session-workbar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
* under the License.
*/

import { COMPOSER_INPUT, test, expect } from './fixtures';
import {
COMPOSER_INPUT,
PARENT_REMOVAL_CHILD_NAME,
PARENT_REMOVAL_PARENT_NAME,
test,
expect,
} from './fixtures';
import type { Page } from '@playwright/test';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
Expand Down Expand Up @@ -485,3 +491,39 @@ test('Side Chat survives collapse, confirms close, and cleans up on source switc
.toBe(false);
await expect(composer).toHaveText('');
});

test('parent Side Chat and its staged quote survive linked child navigation', async ({
parentRemovalWindow: page,
}) => {
await page.getByRole('button', { name: '展开侧边栏' }).click();
const taskList = page.getByRole('navigation', { name: '任务列表' });
await taskList.getByText(PARENT_REMOVAL_PARENT_NAME, { exact: true }).click();
const parentAnswer = page
.getByLabel('Maka 的回答')
.getByText('实现子任务已完成检查。');
await expect(parentAnswer).toBeVisible();
const bounds = await parentAnswer.evaluate((element) => {
const range = document.createRange();
range.selectNodeContents(element);
const rect = range.getBoundingClientRect();
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
});
const selectionY = bounds.y + bounds.height / 2;
await page.mouse.move(bounds.x + 2, selectionY);
await page.mouse.down();
await page.mouse.move(bounds.x + bounds.width - 2, selectionY, { steps: 5 });
await page.mouse.up();
await page.getByRole('button', { name: '在侧栏追问' }).click();
const companion = page.locator('.maka-quote-companion');
const stagedQuote = companion.getByRole('group', { name: '附加内容' });
await expect(stagedQuote).toBeVisible();
const stagedQuoteText = await stagedQuote.textContent();
expect(stagedQuoteText).toBeTruthy();

const parentTurn = parentAnswer.locator('xpath=ancestor::*[@data-turn-id][1]');
await parentTurn.getByRole('button', { name: 'Implementation' }).click();
await expect(page.getByText(PARENT_REMOVAL_CHILD_NAME, { exact: true })).toBeVisible();
await expect(page.getByText('已确认切换到子任务后,父任务的侧边对话应继续保留。')).toBeVisible();
await expect(companion).toBeVisible();
await expect(stagedQuote).toHaveText(stagedQuoteText!);
});
2 changes: 1 addition & 1 deletion apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,7 @@
"react": 1
},
"importSpecifiers": 180,
"nonTriviaTokens": 15620
"nonTriviaTokens": 15618
},
"src/renderer/use-app-shell-composer-quotes.ts": {
"importDeclarations": 3,
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/main/__tests__/quote-companion-retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1919,6 +1919,7 @@ function QuoteCompanionProbe(props: {
const sourceSession = props.sourceSession ?? SOURCE_SESSION;
const companion = useQuoteCompanion({
panelId: 'retry-panel',
sourceSessionId: sourceSession.id,
pendingQuotes: [],
sourceSession,
modelChoices: props.modelChoices ?? [choiceFor(sourceSession)],
Expand Down Expand Up @@ -1947,6 +1948,7 @@ function QuoteCompanionOwnershipProbe(props: {
const sourceSession = props.sourceSession ?? SOURCE_SESSION;
const companion = useQuoteCompanion({
panelId: 'ownership-panel',
sourceSessionId: sourceSession.id,
pendingQuotes: props.pendingQuotes ?? [],
sourceSession,
modelChoices: props.modelChoices ?? [choiceFor(sourceSession)],
Expand Down
Loading