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
4 changes: 4 additions & 0 deletions apps/desktop/e2e-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
"tests": 1,
"electron": "renderer reload is the whole contract: an explicit new task must not reopen history"
},
"partial-history-notice.spec.ts": {
"tests": 1,
"electron": "bounded paging crosses renderer, preload and Host transcript storage; the return-to-tail assertion also uses a real pointer hit-test"
},
"proxy-password-editing.spec.ts": {
"tests": 1,
"electron": "the password never reaches the renderer; only the Host can report passwordConfigured and authenticate offline"
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ type E2eTestFixtures = {
parentRemovalWindow: Page;
railRenderWindow: Page;
promptRailWindow: Page;
partialHistoryWindow: Page;
requestHeaderRowWindow: Page;
newTaskTargetWindow: Page;
directoryReferenceWindow: { page: Page; folder: string };
Expand Down Expand Up @@ -640,6 +641,17 @@ export const test = base.extend<E2eTestFixtures>({
showWindow: true,
}, use);
},
// A transcript larger than the bounded Desktop range. Clicking an unloaded
// prompt exercises the real load-around path and its partial-history UI.
partialHistoryWindow: async ({}, use) => {
await withE2eWindow({
seed: false,
readinessSelector: '[data-turn-id]',
e2eFixtureScenario: 'chat-partial-history',
locale: 'zh-CN',
showWindow: true,
}, use);
},
// Settings → 模型, where `no-models` is the seeded openai-compatible relay —
// the connection type whose detail page owns the custom request headers
// editor. Shown, because what this window is for is a rendered box
Expand Down
84 changes: 84 additions & 0 deletions apps/desktop/e2e/partial-history-notice.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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 { expect, test } from './fixtures';

const GAP = '.maka-transcript-gap-row';
const TURN = '.maka-transcript-turn';

test('bounded transcript ranges expose only their truthful boundary gaps', async ({
partialHistoryWindow: page,
}) => {
await page.setViewportSize({ width: 1_400, height: 800 });

const olderGap = page.locator('[data-transcript-gap="older"]');
const newerGap = page.locator('[data-transcript-gap="newer"]');
await expect(olderGap).toBeVisible();
await expect(olderGap.getByRole('button', {
name: /^(?:加载较早消息|Load earlier messages)$/,
})).toBeVisible();
await expect(newerGap).toHaveCount(0);
await expect(page.locator('.maka-transcript-history-controls')).toHaveCount(0);

const oldestPrompt = page.locator(
'.maka-prompt-rail-tick[data-prompt-turn-id="turn-partial-history-1"]',
);
await expect(oldestPrompt).toBeVisible();
await oldestPrompt.click();

const firstTurn = page.locator('[data-turn-id="turn-partial-history-1"]');
await expect(firstTurn).toBeVisible();
await expect(firstTurn).toHaveAttribute('data-search-highlight', 'true');
await expect(olderGap).toHaveCount(0);
await expect(newerGap).toBeVisible();
await expect(newerGap.getByRole('button', {
name: /^(?:加载较新消息|Load newer messages)$/,
})).toBeVisible();
await expect(page.locator(GAP)).toHaveCount(1);
expect(await page.locator(TURN).count()).toBeLessThanOrEqual(10);

const loadNewer = newerGap.getByRole('button', {
name: /^(?:加载较新消息|Load newer messages)$/,
});
await loadNewer.click();
await expect(page.locator('[data-turn-id="turn-partial-history-2"]')).toBeVisible();
await expect(olderGap).toHaveCount(0);
await expect(newerGap).toBeVisible();
await expect(loadNewer).toBeEnabled();

await loadNewer.click();
await expect(page.locator('[data-turn-id="turn-partial-history-3"]')).toBeVisible();
await expect(olderGap).toBeVisible();
await expect(newerGap).toBeVisible();
await expect(loadNewer).toBeEnabled();
await expect(oldestPrompt).toBeVisible();
await expect(page.locator(GAP)).toHaveCount(2);
expect(await page.locator(TURN).count()).toBeLessThanOrEqual(10);

const returnToLatest = page.getByRole('button', {
name: /^(?:滚动主对话到底部|Scroll main conversation to bottom)$/,
});
await expect(returnToLatest).toBeVisible();
await returnToLatest.click();

await expect(page.locator('[data-turn-id="turn-partial-history-18"]')).toBeVisible();
await expect(newerGap).toHaveCount(0);
await expect(oldestPrompt).toBeVisible();
expect(await page.locator(TURN).count()).toBeLessThanOrEqual(10);
});
2 changes: 1 addition & 1 deletion apps/desktop/e2e/transcript-scroll-cost.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ async function moveToTail(page: Page): Promise<void> {
*/
async function returnToLatest(page: Page): Promise<void> {
const returnLatest = page.getByRole('button', {
name: /^(?:返回最新消息|Return to latest)$/,
name: /^(?:滚动主对话到底部|Scroll main conversation to bottom)$/,
});
await expect(returnLatest).toBeVisible();
await returnLatest.click();
Expand Down
3 changes: 1 addition & 2 deletions apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,7 @@
"react": 1
},
"importSpecifiers": 121,
"nonTriviaTokens": 15007
"nonTriviaTokens": 14996
},
"src/renderer/use-app-shell-composer-quotes.ts": {
"importDeclarations": 2,
Expand Down Expand Up @@ -1157,7 +1157,6 @@
"actionFactories": [],
"dependencyPaths": {
"./chat-recovery-notice": 1,
"./locales/conversation-copy": 1,
"./locales/shell-copy": 1,
"./onboarding-hero": 1,
"./use-app-shell-session-ui-reads": 1,
Expand Down
63 changes: 49 additions & 14 deletions apps/desktop/src/main/__tests__/app-shell-session-ui-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
import {
transcriptReadingPosition,
type TranscriptHistoryGates,
type TranscriptHistoryPending,
} from '../../renderer/features/conversation/index.js';

function boundaryRequest(requestId: string): SandboxBoundaryRequestEvent {
Expand Down Expand Up @@ -102,19 +103,26 @@ function deferredHistoryController() {
}

function crossSessionGateScenario() {
type HistoryRequest = Parameters<typeof transcriptReadingPosition.loadHistory>[0]['request'];
const gates: TranscriptHistoryGates = new WeakMap();
const sessionIds = { a: 'session', b: 'session:a' } as const;
const sides = {
a: deferredHistoryController(),
b: deferredHistoryController(),
};
let active: 'a' | 'b' = 'a';
let range: object = sides.a.controller;
const pending = { a: [] as boolean[], b: [] as boolean[] };
let currentPending: TranscriptHistoryPending | undefined;
const pending = {
a: [] as Array<Pick<HistoryRequest, 'target'> | undefined>,
b: [] as Array<Pick<HistoryRequest, 'target'> | undefined>,
};
const errors = { a: [] as unknown[], b: [] as unknown[] };
return {
sides,
pending,
errors,
currentPending: () => currentPending,
switchTo(id: 'a' | 'b') {
active = id;
range = sides[id].controller;
Expand All @@ -126,11 +134,17 @@ function crossSessionGateScenario() {
const side = sides[id];
return transcriptReadingPosition.loadHistory({
gates,
sessionId: sessionIds[id],
request,
controller: side.controller,
maxBytes: 4096,
isCurrent: () => active === id && range === side.controller,
setPending: (value) => pending[id].push(value),
setPending: (update) => {
currentPending = update(currentPending);
pending[id].push(currentPending?.sessionId === sessionIds[id]
? { target: currentPending.target }
: undefined);
},
onError: (error) => errors[id].push(error),
});
},
Expand Down Expand Up @@ -524,18 +538,19 @@ describe('app shell session UI state controller', () => {
const stale = scenario.load('a', { target: 'earlier' });
await new Promise<void>((resolve) => setImmediate(resolve));
assert.deepEqual(scenario.sides.a.calls, ['before']);
assert.deepEqual(scenario.pending.a, [true]);
assert.deepEqual(scenario.pending.a, [{ target: 'earlier' }]);

scenario.switchTo('b');
const navigation = scenario.load('b', { target: 'latest' });
await new Promise<void>((resolve) => setImmediate(resolve));
assert.deepEqual(scenario.sides.b.calls, ['latest']);
assert.deepEqual(scenario.pending.b, [true]);
assert.deepEqual(scenario.pending.b, [{ target: 'latest' }]);

scenario.sides.b.settleLatest();
scenario.sides.a.settleBefore();
await navigation;
await stale;
assert.deepEqual(scenario.currentPending(), { sessionId: 'session:a', target: 'latest' });
scenario.sides.b.settleLatest();
await navigation;
});

it('leaves the switched-to Session untouched when a stale Session load settles late', async () => {
Expand All @@ -546,13 +561,13 @@ describe('app shell session UI state controller', () => {
const navigation = scenario.load('b', { target: 'latest' });
scenario.sides.b.settleLatest();
await navigation;
assert.deepEqual(scenario.pending.b, [true, false]);
assert.deepEqual(scenario.pending.b, [{ target: 'latest' }, undefined]);
assert.deepEqual(scenario.sides.b.calls, ['latest']);

scenario.sides.a.settleBefore();
await stale;
await new Promise<void>((resolve) => setImmediate(resolve));
assert.deepEqual(scenario.pending.b, [true, false]);
assert.deepEqual(scenario.pending.b, [{ target: 'latest' }, undefined]);
assert.deepEqual(scenario.sides.b.calls, ['latest']);
assert.deepEqual(scenario.errors.b, []);
});
Expand All @@ -565,7 +580,7 @@ describe('app shell session UI state controller', () => {
scenario.sides.a.failBefore(new Error('earlier read failed'));
await stale;
assert.deepEqual(scenario.errors.a, []);
assert.deepEqual(scenario.pending.a, [true, false]);
assert.deepEqual(scenario.pending.a, [{ target: 'earlier' }, undefined]);
assert.deepEqual(scenario.pending.b, []);
});

Expand All @@ -582,7 +597,12 @@ describe('app shell session UI state controller', () => {
assert.deepEqual(scenario.sides.a.calls, ['before', 'latest']);
scenario.sides.a.settleLatest();
await Promise.allSettled([queuedEarlier, queuedLatest]);
assert.deepEqual(scenario.pending.a, [true, false, true, false]);
assert.deepEqual(scenario.pending.a, [
{ target: 'earlier' },
undefined,
{ target: 'latest' },
undefined,
]);
});

it('replays a queued forward load with its reading anchor after a backward load settles', async () => {
Expand All @@ -596,7 +616,12 @@ describe('app shell session UI state controller', () => {
assert.deepEqual(scenario.sides.a.calls, ['before', 'after:4096:turn-anchor']);
scenario.sides.a.settleAfter();
await queued;
assert.deepEqual(scenario.pending.a, [true, false, true, false]);
assert.deepEqual(scenario.pending.a, [
{ target: 'earlier' },
undefined,
{ target: 'later' },
undefined,
]);
});

it('keeps the queued latest load when adjacent requests arrive after it', async () => {
Expand All @@ -613,7 +638,12 @@ describe('app shell session UI state controller', () => {
assert.deepEqual(scenario.sides.a.calls, ['before', 'latest']);
scenario.sides.a.settleLatest();
await Promise.allSettled([queuedLatest, queuedEarlier, queuedLater]);
assert.deepEqual(scenario.pending.a, [true, false, true, false]);
assert.deepEqual(scenario.pending.a, [
{ target: 'earlier' },
undefined,
{ target: 'latest' },
undefined,
]);
});

it('does not replay a settled load after its Session range was replaced', async () => {
Expand All @@ -627,7 +657,7 @@ describe('app shell session UI state controller', () => {
await new Promise<void>((resolve) => setImmediate(resolve));
assert.deepEqual(scenario.sides.a.calls, ['before']);
assert.deepEqual(scenario.sides.b.calls, []);
assert.deepEqual(scenario.pending.a, [true, false]);
assert.deepEqual(scenario.pending.a, [{ target: 'earlier' }, undefined]);
scenario.sides.a.settleLatest();
await queued;
});
Expand All @@ -642,7 +672,12 @@ describe('app shell session UI state controller', () => {
assert.deepEqual(scenario.sides.a.calls, ['before', 'latest']);
scenario.sides.a.settleLatest();
await queued;
assert.deepEqual(scenario.pending.a, [true, false, true, false]);
assert.deepEqual(scenario.pending.a, [
{ target: 'earlier' },
undefined,
{ target: 'latest' },
undefined,
]);
assert.deepEqual(scenario.pending.b, []);
assert.deepEqual(scenario.sides.b.calls, []);
});
Expand Down
13 changes: 13 additions & 0 deletions apps/desktop/src/main/e2e-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@ import {
LONG_SIDEBAR_PROJECT_ID,
LONG_SIDEBAR_PROJECT_NAME,
LONG_SIDEBAR_SESSION_PREFIX,
PARTIAL_HISTORY_SESSION_ID,
PROMPT_RAIL_SESSION_ID,
TURN_SESSION_ID,
writeSession,
} from './e2e-fixture/seed-helpers.js';
import {
partialHistoryMessages,
partialHistorySession,
promptRailMessages,
promptRailSession,
turnMessages,
Expand All @@ -60,6 +63,7 @@ const E2E_FIXTURE_SCENARIOS = new Set<E2eFixtureScenario>([
'turn-narrative',
'turn-narrative-browser',
'chat-prompt-rail',
'chat-partial-history',
'settings-data',
'settings-bots-onboarding',
'settings-general',
Expand Down Expand Up @@ -169,6 +173,8 @@ export function getE2eFixtureState(fixture: E2eFixture | null): E2eFixtureState
// Workbar collapsed: the rail lives on the chat scrollport's right edge,
// and the panel would take the width the measurements are about.
return { ...state, activeSessionId: PROMPT_RAIL_SESSION_ID, workbarCollapsed: true };
case 'chat-partial-history':
return { ...state, activeSessionId: PARTIAL_HISTORY_SESSION_ID, workbarCollapsed: true };
case 'settings-data':
return { ...state, activeSessionId: TURN_SESSION_ID, openSettingsSection: 'data' };
case 'settings-bots-onboarding':
Expand Down Expand Up @@ -224,6 +230,13 @@ export async function seedE2eFixture(input: {
if (scenario === 'chat-prompt-rail') {
await writeSession(input.workspaceRoot, promptRailSession(now), promptRailMessages(now));
}
if (scenario === 'chat-partial-history') {
await writeSession(
input.workspaceRoot,
partialHistorySession(now),
partialHistoryMessages(now),
);
}
if (scenario === 'sidebar-search-modal-open') {
for (const seed of longSidebarSessions(now)) {
await writeSession(input.workspaceRoot, seed.header, seed.messages);
Expand Down
Loading