Skip to content
Merged
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
59 changes: 59 additions & 0 deletions apps/desktop/e2e/composer-plus-menu-stability.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ declare global {
makaE2eLatch?: {
arm(key: LatchKey, options?: { oneShot?: boolean }): void;
release(key: LatchKey): void;
reject(key: LatchKey, message: string): void;
};
}
}
Expand Down Expand Up @@ -58,6 +59,16 @@ async function releaseBridgeLatch(
await page.evaluate((latchKey) => window.makaE2eLatch?.release(latchKey), key);
}

async function rejectBridgeLatch(
page: import('@playwright/test').Page,
key: LatchKey,
): Promise<void> {
await page.evaluate(
(latchKey) => window.makaE2eLatch?.reject(latchKey, 'forced E2E bridge failure'),
key,
);
}

/**
* Toggling Plan from the + menu must not move the menu.
*
Expand Down Expand Up @@ -282,6 +293,54 @@ test('two rapid Plan toggles land on the last requested state', async ({
await expect(planRow).toHaveAttribute('aria-checked', 'false');
});

test('a failed catalog refresh keeps the committed Plan state visible', async ({
invocableSkillsWindow: page,
}) => {
const composer = page.locator(COMPOSER_INPUT);
await composer.fill('alpha-marker');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: alpha-marker/)).toBeVisible();

await page.getByRole('button', { name: '添加上下文' }).click();
const menu = page.getByRole('menu', { name: '添加上下文' });
const planRow = menu.getByRole('menuitemcheckbox', { name: 'Plan' });
await expect(planRow).toHaveAttribute('aria-checked', 'false');
await expect(planRow).not.toHaveAttribute('aria-disabled', 'true');

await armBridgeLatch(page, 'sessions.list', { oneShot: true });
await planRow.click();
await expect(planRow).toHaveAttribute('aria-checked', 'true');

await rejectBridgeLatch(page, 'sessions.list');
await expect(planRow).toHaveAttribute('aria-checked', 'true');
});

test('latest Plan intent still reaches the Host after a catalog refresh fails', async ({
invocableSkillsWindow: page,
}) => {
const composer = page.locator(COMPOSER_INPUT);
await composer.fill('alpha-marker');
await composer.press('Enter');
await expect(page.getByText(/Fake backend received: alpha-marker/)).toBeVisible();

await page.getByRole('button', { name: '添加上下文' }).click();
const planRow = page.getByRole('menu', { name: '添加上下文' })
.getByRole('menuitemcheckbox', { name: 'Plan' });
await expect(planRow).not.toHaveAttribute('aria-disabled', 'true');

await armBridgeLatch(page, 'sessions.list', { oneShot: true });
await planRow.click();
await expect(planRow).toHaveAttribute('aria-checked', 'true');
await planRow.click();
await rejectBridgeLatch(page, 'sessions.list');

await expect.poll(async () => page.evaluate(async () => {
const sessions = await window.maka.sessions.list();
return sessions[0]?.collaborationMode;
})).toBe('agent');
await expect(planRow).toHaveAttribute('aria-checked', 'false');
});

test('deleting the session while a toggle is pending settles clean', async ({
invocableSkillsWindow: page,
}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ function createActionsDeps() {
setInteractionBySession: () => undefined,
showModelSetupToast: () => undefined,
toastApi: { error: () => undefined, info: () => undefined },
upsertSessionSummary: () => undefined,
newChatModel: null,
pendingNewChatThinkingLevel: null,
newChatPermissionChoice: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ function createActionsDeps() {
setInteractionBySession: () => undefined,
showModelSetupToast: () => undefined,
toastApi: { error: () => undefined, info: () => undefined },
upsertSessionSummary: () => undefined,
newChatModel: null,
pendingNewChatThinkingLevel: null,
newChatPermissionChoice: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,6 @@ function createHarness(options: {
for (const key of Object.keys(pendingBySession)) delete pendingBySession[key];
Object.assign(pendingBySession, next);
},
setSessions: (update) => {
sessionsRef.current = update(sessionsRef.current);
},
toastApi: {
success: (title, description) => successes.push({ title, description }),
error: (title, _description, _details, target) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,7 @@ import type { SessionEventStreamSnapshot } from '@maka/core/session-event-health
import type { SessionSummary } from '@maka/core/session';
import { armLiveTurn, confirmLiveTurn } from '@maka/ui';
import { settledSessionTransientIds } from '../../renderer/settled-session-transients.js';
import {
mergeSessionSummaryListForDisplay,
mergeSessionSummaryForDisplay,
normalizeSessionSummaryForDisplay,
} from '../../renderer/session-status-presentation.js';
import { normalizeSessionSummaryForDisplay } from '../../renderer/session-status-presentation.js';
import {
clearAppShellSessionUiStateForSession,
createAppShellSessionUiStateController,
Expand Down Expand Up @@ -80,36 +76,6 @@ function seededState(): AppShellSessionUiState {
}

describe('session live run display state', () => {
it('preserves known live state when a mutation response omits it', () => {
const current = {
id: 'session-1',
status: 'running',
runningTurnIds: ['turn-live'],
} as SessionSummary;
const mutation = { id: 'session-1', status: 'running' } as SessionSummary;

assert.deepEqual(mergeSessionSummaryForDisplay(current, mutation).runningTurnIds, [
'turn-live',
]);
});

it('lets known-empty replace prior running state and clear a stale running status', () => {
const current = {
id: 'session-1',
status: 'running',
runningTurnIds: ['turn-live'],
} as SessionSummary;
const catalog = {
id: 'session-1',
status: 'running',
runningTurnIds: [],
} as unknown as SessionSummary;

const merged = mergeSessionSummaryForDisplay(current, catalog);
assert.deepEqual(merged.runningTurnIds, []);
assert.equal(merged.status, 'active');
});

it('keeps persisted running as a fallback only while live state is unknown', () => {
const unknown = { id: 'unknown', status: 'running' } as SessionSummary;
const knownEmpty = {
Expand All @@ -122,22 +88,6 @@ describe('session live run display state', () => {
assert.equal(normalizeSessionSummaryForDisplay(knownEmpty).status, 'active');
});

it('preserves live authority when the list state boundary accepts a metadata replacement', () => {
const current = {
id: 'session-live',
status: 'running',
runningTurnIds: ['turn-live'],
} as SessionSummary;
const mutation = {
id: 'session-live',
status: 'running',
permissionMode: 'bypass',
} as SessionSummary;

assert.deepEqual(mergeSessionSummaryListForDisplay([current], [mutation]), [
{ ...mutation, runningTurnIds: ['turn-live'] },
]);
});
});

describe('app shell session UI state controller', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ test('preserves a Branch copy identity after an ambiguous failure and completes
refreshSessions: async () => [],
setMessages: () => undefined,
toastApi: { info() {}, success() {}, error() {} },
upsertSessionSummary: () => undefined,
});

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,6 @@ function continuitySnapshot() {
metadataRevision: 1,
status: 'running' as const,
createdAt: 1,
lastUsedAt: 1,
isArchived: false,
},
projectionRevision: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ function session(
hostCwd: '/workspace',
},
createdAt: 1,
lastUsedAt: 1,
activityAt: 1,
name: id,
isFlagged: false,
isArchived: false,
Expand Down Expand Up @@ -460,7 +460,6 @@ function continuitySnapshot(rootTurn: TurnSnapshot | null): SessionContinuitySna
metadataRevision: 1,
status: rootTurn ? 'running' : 'active',
createdAt: 1,
lastUsedAt: 1,
isArchived: false,
},
projectionRevision: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,7 @@ function session(
hostCwd: '/workspace',
},
createdAt: 1,
lastUsedAt: 1,
activityAt: 1,
name: id,
isFlagged: false,
isArchived: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ function session(
hostCwd: '/workspace',
},
createdAt: 1,
lastUsedAt: 1,
activityAt: 1,
name: 'Desktop Host Session',
isFlagged: false,
isArchived: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ function subscription(
metadataRevision: 1,
status: 'active',
createdAt: 1,
lastUsedAt: 1,
isArchived: false,
},
projectionRevision: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1100,7 +1100,6 @@ function continuitySnapshot(
metadataRevision: 1,
status: 'running',
createdAt: 1,
lastUsedAt: 1,
isArchived: false,
},
projectionRevision: 1,
Expand Down Expand Up @@ -1230,7 +1229,7 @@ function session(id: string): SessionCatalogProjection {
hostCwd: '/workspace',
},
createdAt: 1,
lastUsedAt: 1,
activityAt: 1,
name: id,
isFlagged: false,
isArchived: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ function session(id: string): SessionCatalogProjection {
hostCwd: '/workspace',
},
createdAt: 1,
lastUsedAt: 1,
activityAt: 1,
name: 'Imported',
isFlagged: false,
isArchived: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ function catalogSession(id: string, name: string): SessionCatalogProjection {
hostCwd: '/workspace',
},
createdAt: 1,
lastUsedAt: 1,
activityAt: 1,
lastMessageAt: 1,
name,
isFlagged: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function projection(overrides: Partial<SessionCatalogProjection> = {}): SessionC
hostCwd: '/workspace',
},
createdAt: 1,
lastUsedAt: 2,
activityAt: 2,
name: 'Session',
isFlagged: false,
isArchived: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import test from 'node:test';
import type { DesktopSessionSummary } from '../../preload/bridge-contract.js';
import { collectRuntimeHostSessionCatalogs } from '../../preload/runtime-host-session-catalog.js';

function session(id: string, activityAt: number): DesktopSessionSummary {
return { id, activityAt } as DesktopSessionSummary;
}

test('keeps healthy Host catalogs when another Host rejects', async () => {
const sessions = await collectRuntimeHostSessionCatalogs([
Promise.resolve([session('older', 1)]),
Promise.reject(new Error('remote unavailable')),
Promise.resolve([session('newer', 2)]),
]);

assert.deepEqual(sessions.map(({ id }) => id), ['newer', 'older']);
});

test('fails when every Host catalog rejects', async () => {
await assert.rejects(
collectRuntimeHostSessionCatalogs([
Promise.reject(new Error('first unavailable')),
Promise.reject(new Error('second unavailable')),
]),
/Every Runtime Host Session Catalog request failed/,
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function session(id: string): SessionCatalogProjection {
hostCwd: '/workspace',
},
createdAt: 1,
lastUsedAt: 1,
activityAt: 1,
name: id,
isFlagged: false,
isArchived: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,6 @@ function observerWithTranscript(
metadataRevision: 1,
status: "running",
createdAt: 1,
lastUsedAt: 1,
isArchived: false,
},
projectionRevision: 1,
Expand Down Expand Up @@ -1084,7 +1083,7 @@ function session(cwd = "/workspace"): SessionCatalogProjection {
hostCwd: cwd,
},
createdAt: 1,
lastUsedAt: 1,
activityAt: 1,
name: "Session",
isFlagged: false,
isArchived: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2468,7 +2468,6 @@ function continuitySnapshot(
metadataRevision: 1,
status: "running",
createdAt: 1,
lastUsedAt: 1,
isArchived: false,
},
projectionRevision: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ function projection(sessionId: string) {
labels: [],
status: 'active' as const,
createdAt: 1,
lastUsedAt: 1,
backend: 'fake' as const,
llmConnectionSlug: 'fake',
connectionLocked: false,
Expand Down
Loading