merge: integrate foreground conversation reconciliation

# Conflicts:
#	tests/e2e/pi-coding-first-chat.spec.ts
This commit is contained in:
inman
2026-09-03 10:47:56 +08:00
6 changed files with 439 additions and 22 deletions

View File

@@ -19,6 +19,11 @@ type HostConnection = {
async function disableCodingEventSource(page: Page): Promise<void> {
await page.addInitScript(() => {
const sources = new Set<LocalEventSource>();
type TrackedWindow = Window & {
__makeloreCodingEventSources?: LocalEventSource[];
};
const trackedWindow = window as TrackedWindow;
trackedWindow.__makeloreCodingEventSources = [];
class LocalEventSource extends EventTarget {
static readonly CONNECTING = 0;
static readonly OPEN = 1;
@@ -37,6 +42,7 @@ async function disableCodingEventSource(page: Page): Promise<void> {
super();
this.url = url;
sources.add(this);
trackedWindow.__makeloreCodingEventSources?.push(this);
queueMicrotask(() => this.onopen?.(new Event('open')));
}
@@ -86,6 +92,7 @@ async function installCodingFirstChatHost(
abortRequested: boolean;
releaseSnapshot: (() => void) | null;
snapshotPending: boolean;
snapshotSettled: boolean;
};
const mainGlobal = globalThis as typeof globalThis & {
__makelorePiFirstChatE2E?: MainState;
@@ -97,6 +104,7 @@ async function installCodingFirstChatHost(
abortRequested: false,
releaseSnapshot: null,
snapshotPending: false,
snapshotSettled: false,
};
mainGlobal.__makelorePiFirstChatE2E = state;
const now = '2026-08-24T00:00:00.000Z';
@@ -530,11 +538,28 @@ async function installCodingFirstChatHost(
await new Promise<void>((resolve) => { state.releaseSnapshot = resolve; });
state.snapshotPending = false;
}
const currentSnapshot = state.snapshotSettled
? {
...snapshot,
run: {
status: 'idle',
runId: 'run-e2e-feature',
mode: 'prompt',
startedAt: 1_000,
settledAt: 2_000,
terminalReason: 'aborted',
},
queue: { items: [] },
pendingInteractions: [],
worker: { status: 'stopped', generation: 1 },
cursor: { workerGeneration: 1, seq: 1 },
}
: snapshot;
return respond({
snapshot: state.abortRequested
? {
...snapshot,
nodes: snapshot.nodes.map((node) => {
...currentSnapshot,
nodes: currentSnapshot.nodes.map((node) => {
if (node.kind === 'message' && node.role === 'assistant') {
return {
...node,
@@ -567,8 +592,8 @@ async function installCodingFirstChatHost(
cursor: { workerGeneration: 1, seq: 1 },
}
: state.interactionAnswered
? { ...snapshot, pendingInteractions: [] }
: snapshot,
? { ...currentSnapshot, pendingInteractions: [] }
: currentSnapshot,
});
}
if (path === `/api/coding/conversations/${secondConversation.id}/snapshot`) {
@@ -652,6 +677,17 @@ async function releaseSnapshot(electronApp: ElectronApplication): Promise<void>
});
}
async function settleSnapshot(electronApp: ElectronApplication): Promise<void> {
await electronApp.evaluate(() => {
const mainGlobal = globalThis as typeof globalThis & {
__makelorePiFirstChatE2E?: { snapshotSettled: boolean };
};
if (mainGlobal.__makelorePiFirstChatE2E) {
mainGlobal.__makelorePiFirstChatE2E.snapshotSettled = true;
}
});
}
test('first PI Conversation is editable under 500 ms and submits before runtime Snapshot', async ({
launchElectronApp,
}) => {
@@ -903,6 +939,60 @@ test('hidden Conversation badge ignores process failures until interaction or ta
}
});
test('foreground focus rehydrates a terminal Snapshot after lifecycle sleep', async ({
launchElectronApp,
}) => {
const electronApp = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(electronApp);
const hostConnection = 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, hostConnection, true);
await disableCodingEventSource(page);
await page.reload();
page = await getStableWindow(electronApp);
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('main-layout')).toBeVisible();
await page.evaluate(() => { window.location.hash = '/chat'; });
const processGroup = page.getByTestId('coding-process-group');
await expect(processGroup).toHaveAttribute('data-process-state', 'active');
await expect(processGroup.locator('summary').first()).toContainText('处理中');
const snapshotPath = '/api/coding/conversations/conversation-pi-first-chat/snapshot';
const stateBeforeSleep = await readState(electronApp);
const snapshotCallsBeforeSleep = stateBeforeSleep.captured.filter((request) => (
request.path === snapshotPath
)).length;
await settleSnapshot(electronApp);
await electronApp.evaluate(({ BrowserWindow }) => {
for (const window of BrowserWindow.getAllWindows()) {
window.webContents.send('lifecycle:sleep');
}
});
await expect.poll(async () => page.evaluate(() => {
const trackedWindow = window as typeof window & {
__makeloreCodingEventSources?: Array<{ readyState: number }>;
};
return trackedWindow.__makeloreCodingEventSources?.at(-1)?.readyState ?? -1;
})).toBe(2);
await page.evaluate(() => window.dispatchEvent(new FocusEvent('focus')));
await expect(processGroup).toHaveAttribute('data-process-state', 'settled');
await expect(processGroup.locator('summary').first()).not.toContainText('处理中');
await expect.poll(async () => {
const state = await readState(electronApp);
return state.captured.filter((request) => request.path === snapshotPath).length;
}).toBeGreaterThan(snapshotCallsBeforeSleep);
const stateAfterFocus = await readState(electronApp);
expect(stateAfterFocus.captured.some((request) => (
request.path === '/api/coding/conversations/conversation-pi-first-chat/prompt'
))).toBe(false);
});
test('PI feature UI isolates Conversations and exposes queue, interaction, model, and subagent state', async ({
launchElectronApp,
}) => {