547 lines
22 KiB
TypeScript
547 lines
22 KiB
TypeScript
import type { ElectronApplication, Page } from 'playwright-core';
|
|
import { expect, getStableWindow, test } from './fixtures/electron';
|
|
|
|
type CapturedRequest = {
|
|
path: string;
|
|
method: string;
|
|
body?: Record<string, unknown>;
|
|
byteLength?: number;
|
|
contentType?: string;
|
|
at: number;
|
|
};
|
|
|
|
type HostConnection = {
|
|
baseUrl: string;
|
|
token: string;
|
|
};
|
|
|
|
async function disableCodingEventSource(page: Page): Promise<void> {
|
|
await page.addInitScript(() => {
|
|
class LocalEventSource extends EventTarget {
|
|
static readonly CONNECTING = 0;
|
|
static readonly OPEN = 1;
|
|
static readonly CLOSED = 2;
|
|
readonly CONNECTING = LocalEventSource.CONNECTING;
|
|
readonly OPEN = LocalEventSource.OPEN;
|
|
readonly CLOSED = LocalEventSource.CLOSED;
|
|
readonly url: string;
|
|
readonly withCredentials = false;
|
|
readyState = LocalEventSource.OPEN;
|
|
onopen: ((event: Event) => void) | null = null;
|
|
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
onerror: ((event: Event) => void) | null = null;
|
|
|
|
constructor(url: string) {
|
|
super();
|
|
this.url = url;
|
|
queueMicrotask(() => this.onopen?.(new Event('open')));
|
|
}
|
|
|
|
close(): void {
|
|
this.readyState = LocalEventSource.CLOSED;
|
|
}
|
|
}
|
|
|
|
Object.defineProperty(window, 'EventSource', {
|
|
configurable: true,
|
|
writable: true,
|
|
value: LocalEventSource,
|
|
});
|
|
});
|
|
}
|
|
|
|
async function installCodingFirstChatHost(
|
|
electronApp: ElectronApplication,
|
|
hostConnection: HostConnection,
|
|
featureComplete = false,
|
|
): Promise<void> {
|
|
await electronApp.evaluate(async (_, payload) => {
|
|
const { connection, featureComplete } = payload;
|
|
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');
|
|
type MainState = {
|
|
captured: CapturedRequest[];
|
|
releaseSnapshot: (() => void) | null;
|
|
snapshotPending: boolean;
|
|
};
|
|
const mainGlobal = globalThis as typeof globalThis & {
|
|
__makelorePiFirstChatE2E?: MainState;
|
|
};
|
|
const state: MainState = {
|
|
captured: [],
|
|
releaseSnapshot: null,
|
|
snapshotPending: false,
|
|
};
|
|
mainGlobal.__makelorePiFirstChatE2E = state;
|
|
const now = '2026-08-24T00:00:00.000Z';
|
|
const project = {
|
|
id: 'project-pi-first-chat',
|
|
name: 'PI first chat',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
lastOpenedAt: now,
|
|
};
|
|
const legacyProject = { ...project, path: 'D:/e2e/pi-first-chat' };
|
|
const agent = {
|
|
id: 'builder',
|
|
avatarId: 'avatar-01',
|
|
roleName: '实现者',
|
|
name: 'Builder',
|
|
builtIn: false,
|
|
enabled: true,
|
|
model: null,
|
|
modelResolution: 'required',
|
|
skillIds: [],
|
|
responsibility: {
|
|
mission: 'Implement',
|
|
owns: [],
|
|
boundaries: [],
|
|
collaborators: [],
|
|
principles: [],
|
|
},
|
|
prompt: '',
|
|
archivedAt: null,
|
|
pinned: true,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
const config = {
|
|
schemaVersion: 2,
|
|
projectType: 'custom',
|
|
initialized: true,
|
|
agents: [agent],
|
|
knowledgeDirectory: 'knowledge',
|
|
legacyConversationNotice: 'none',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
const legacyConfig = {
|
|
schemaVersion: 1,
|
|
projectType: 'custom',
|
|
initialized: true,
|
|
defaultModel: null,
|
|
agents: [{ ...agent, model: null }],
|
|
knowledgeDirectory: 'knowledge',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
const conversation = {
|
|
id: 'conversation-pi-first-chat',
|
|
agentId: agent.id,
|
|
title: '新对话',
|
|
model: null,
|
|
modelResolution: 'required',
|
|
archivedAt: null,
|
|
unread: false,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
const secondConversation = {
|
|
...conversation,
|
|
id: 'conversation-pi-second',
|
|
title: 'Second Conversation',
|
|
updatedAt: '2026-08-23T23:59:00.000Z',
|
|
model: featureComplete
|
|
? { accountId: 'account-e2e', modelId: 'model-b', thinkingLevel: 'low' }
|
|
: null,
|
|
modelResolution: featureComplete ? 'resolved' : 'required',
|
|
};
|
|
const snapshot = {
|
|
schemaVersion: 1,
|
|
conversation: {
|
|
id: conversation.id,
|
|
projectId: project.id,
|
|
agentId: agent.id,
|
|
title: conversation.title,
|
|
model: { model: null, modelResolution: 'required' },
|
|
},
|
|
nodes: featureComplete ? [{
|
|
kind: 'subagent',
|
|
id: 'subagent-e2e',
|
|
runId: 'run-e2e-feature',
|
|
details: {
|
|
schema: 'subagent.v1',
|
|
dispatchId: 'dispatch-e2e',
|
|
mode: 'parallel',
|
|
tasks: [
|
|
{ taskId: 'task-reader', agentId: 'reader', toolProfile: 'read-only', status: 'complete', summary: 'Read complete' },
|
|
{ taskId: 'task-builder', agentId: 'builder', toolProfile: 'coding', status: 'running' },
|
|
],
|
|
},
|
|
}] : [],
|
|
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: [] }] : [] },
|
|
context: featureComplete
|
|
? { usedTokens: 256, contextWindow: 4096, compaction: 'idle' }
|
|
: { usedTokens: 0, contextWindow: 0, compaction: 'idle' },
|
|
pendingInteractions: featureComplete ? [{
|
|
id: 'interaction-e2e',
|
|
conversationId: conversation.id,
|
|
runId: 'run-e2e-feature',
|
|
kind: 'confirm',
|
|
title: '允许继续?',
|
|
message: '确认当前实现方向。',
|
|
status: 'pending',
|
|
}] : [],
|
|
worker: { status: 'ready', generation: 1 },
|
|
cursor: { workerGeneration: 1, seq: 0 },
|
|
};
|
|
const secondSnapshot = {
|
|
...snapshot,
|
|
conversation: {
|
|
...snapshot.conversation,
|
|
id: secondConversation.id,
|
|
title: secondConversation.title,
|
|
model: {
|
|
model: secondConversation.model,
|
|
modelResolution: secondConversation.modelResolution,
|
|
},
|
|
},
|
|
nodes: [],
|
|
run: { status: 'idle' },
|
|
queue: { items: [] },
|
|
pendingInteractions: [],
|
|
};
|
|
const respond = (json: unknown, status = 200) => ({
|
|
ok: true,
|
|
data: { status, ok: status >= 200 && status < 300, json },
|
|
});
|
|
|
|
ipcMain.removeHandler('hostapi:fetch');
|
|
ipcMain.handle('hostapi:fetch', async (
|
|
_event,
|
|
request: {
|
|
path?: string;
|
|
method?: string;
|
|
headers?: Record<string, string>;
|
|
body?: unknown;
|
|
},
|
|
) => {
|
|
const path = request.path ?? '';
|
|
const method = request.method ?? 'GET';
|
|
const body = typeof request.body === 'string' && request.body
|
|
? JSON.parse(request.body) as Record<string, unknown>
|
|
: undefined;
|
|
const binaryBody = request.body instanceof ArrayBuffer
|
|
? new Uint8Array(request.body)
|
|
: ArrayBuffer.isView(request.body)
|
|
? new Uint8Array(
|
|
request.body.buffer,
|
|
request.body.byteOffset,
|
|
request.body.byteLength,
|
|
)
|
|
: undefined;
|
|
state.captured.push({
|
|
path,
|
|
method,
|
|
...(body ? { body } : {}),
|
|
...(binaryBody ? { byteLength: binaryBody.byteLength } : {}),
|
|
...(request.headers?.['Content-Type']
|
|
? { contentType: request.headers['Content-Type'] }
|
|
: {}),
|
|
at: Date.now(),
|
|
});
|
|
|
|
if (path === '/api/coding/attachments'
|
|
|| /^\/api\/coding\/attachments\/[^/]+\/content$/.test(path)) {
|
|
const response = await fetch(`${connection.baseUrl}${path}`, {
|
|
method,
|
|
headers: {
|
|
Authorization: `Bearer ${connection.token}`,
|
|
...(request.headers ?? {}),
|
|
},
|
|
...(binaryBody ? { body: binaryBody } : {}),
|
|
});
|
|
const responseContentType = response.headers.get('content-type') ?? '';
|
|
if (responseContentType.includes('application/json')) {
|
|
return {
|
|
ok: true,
|
|
data: {
|
|
status: response.status,
|
|
ok: response.ok,
|
|
json: await response.json(),
|
|
transport: 'loopback',
|
|
},
|
|
};
|
|
}
|
|
return {
|
|
ok: true,
|
|
data: {
|
|
status: response.status,
|
|
ok: response.ok,
|
|
bytes: new Uint8Array(await response.arrayBuffer()),
|
|
contentType: responseContentType.split(';', 1)[0]?.trim(),
|
|
transport: 'loopback',
|
|
},
|
|
};
|
|
}
|
|
|
|
if (path === '/api/coding/projects') {
|
|
return respond({ projects: [project], activeProjectId: project.id });
|
|
}
|
|
if (path === '/api/provider-accounts') {
|
|
return respond(featureComplete ? [{
|
|
id: 'account-e2e',
|
|
vendorId: 'custom',
|
|
label: 'E2E account',
|
|
authMode: 'api_key',
|
|
model: 'model-a',
|
|
fallbackModels: ['model-b'],
|
|
enabled: true,
|
|
isDefault: true,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
}] : []);
|
|
}
|
|
if (path === '/api/provider-accounts/key-info') return respond([]);
|
|
if (path === '/api/provider-vendors') return respond(featureComplete ? [{ id: 'custom', name: 'Custom' }] : []);
|
|
if (path === '/api/provider-accounts/default') return respond({ accountId: featureComplete ? 'account-e2e' : null });
|
|
if (path === `/api/coding/projects/config?projectId=${project.id}`) {
|
|
return respond({ snapshot: { project, config, knowledgeFiles: [] } });
|
|
}
|
|
if (path === `/api/coding/projects/conversations?projectId=${project.id}`) {
|
|
return respond({ conversations: featureComplete ? [conversation, secondConversation] : [] });
|
|
}
|
|
if (path === '/api/coding/projects/conversations' && method === 'POST') {
|
|
return respond({ conversation }, 201);
|
|
}
|
|
if (path === `/api/coding/conversations/${conversation.id}/snapshot`) {
|
|
if (!featureComplete) {
|
|
state.snapshotPending = true;
|
|
await new Promise<void>((resolve) => { state.releaseSnapshot = resolve; });
|
|
state.snapshotPending = false;
|
|
}
|
|
return respond({ snapshot });
|
|
}
|
|
if (path === `/api/coding/conversations/${secondConversation.id}/snapshot`) {
|
|
return respond({ snapshot: secondSnapshot });
|
|
}
|
|
if (path === `/api/coding/conversations/${conversation.id}/prompt` && method === 'POST') {
|
|
return respond({
|
|
acceptance: {
|
|
accepted: true,
|
|
conversationId: conversation.id,
|
|
clientRequestId: body?.clientRequestId,
|
|
runId: 'run-e2e-1',
|
|
mode: 'prompt',
|
|
},
|
|
}, 202);
|
|
}
|
|
if (/^\/api\/coding\/conversations\/[^/]+\/(abort|compact|recover)$/.test(path) && method === 'POST') {
|
|
return respond({});
|
|
}
|
|
if (/^\/api\/coding\/conversations\/[^/]+\/model$/.test(path) && method === 'POST') {
|
|
return respond({ model: { model: body?.model, modelResolution: 'resolved' } });
|
|
}
|
|
if (/^\/api\/coding\/conversations\/[^/]+\/thinking$/.test(path) && method === 'POST') {
|
|
return respond({ model: snapshot.conversation.model });
|
|
}
|
|
if (/^\/api\/coding\/conversations\/[^/]+\/fork$/.test(path) && method === 'POST') {
|
|
return respond({ conversation: { ...conversation, id: 'conversation-forked', title: 'Feature UI branch' } }, 201);
|
|
}
|
|
if (/^\/api\/coding\/conversations\/[^/]+$/.test(path) && method === 'PATCH') {
|
|
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') {
|
|
return respond({});
|
|
}
|
|
if (/^\/api\/coding\/conversations\/[^/]+\/changes$/.test(path)) {
|
|
return respond({ changes: { conversationId: conversation.id, runId: 'run-e2e-feature', git: true, baselineHead: 'head-e2e', files: [{ path: 'src/app.ts', status: 'modified', diff: '+feature UI' }] } });
|
|
}
|
|
if (path === '/api/coding/files/status') return respond({ files: [{ path: 'src/app.ts', name: 'app.ts', type: 'file', status: 'modified' }] });
|
|
if (path.startsWith('/api/coding/files/content?')) return respond({ file: { path: 'src/app.ts', content: 'export const app = true;', truncated: false } });
|
|
if (path.startsWith('/api/coding/files/find?')) return respond({ files: [{ path: 'src/app.ts', name: 'app.ts', type: 'file' }] });
|
|
if (path.startsWith('/api/coding/skills')) return respond({ skills: [{ id: 'research', name: 'Research', description: 'Inspect sources', selected: true }] });
|
|
if (/^\/api\/coding\/conversations\/[^/]+\/commands$/.test(path)) return respond({ commands: [{ name: 'review', title: 'Review', description: 'Review changes', source: 'makelore' }] });
|
|
if (path === '/api/coding/runtime/diagnostics') return respond({ runtime: { revision: { provider: 1, resources: 1 }, workers: [{ conversationId: conversation.id, generation: 1, state: 'running', stage: 'running' }] } });
|
|
if (path.startsWith('/api/opencode/status')) {
|
|
return respond({ state: 'stopped', port: null, url: null });
|
|
}
|
|
if (path === '/api/opencode/projects' || path.startsWith('/api/opencode/projects?')) {
|
|
return respond({ projects: [legacyProject], activeProject: legacyProject });
|
|
}
|
|
if (path === '/api/opencode/projects/active') {
|
|
return respond({ projects: [legacyProject], activeProject: legacyProject });
|
|
}
|
|
if (path.startsWith('/api/opencode/projects/config?')) {
|
|
return respond({ status: 'valid', config: legacyConfig, knowledgeFiles: [] });
|
|
}
|
|
if (path.startsWith('/api/opencode/projects/conversations?')) {
|
|
return respond({
|
|
state: { schemaVersion: 1, sessions: [], updatedAt: now },
|
|
});
|
|
}
|
|
return respond({ success: false, error: `Unhandled E2E route: ${method} ${path}` }, 404);
|
|
});
|
|
}, { connection: hostConnection, featureComplete });
|
|
}
|
|
|
|
async function readState(electronApp: ElectronApplication): Promise<{
|
|
captured: CapturedRequest[];
|
|
snapshotPending: boolean;
|
|
}> {
|
|
return await electronApp.evaluate(() => {
|
|
const mainGlobal = globalThis as typeof globalThis & {
|
|
__makelorePiFirstChatE2E?: {
|
|
captured: CapturedRequest[];
|
|
snapshotPending: boolean;
|
|
};
|
|
};
|
|
return structuredClone({
|
|
captured: mainGlobal.__makelorePiFirstChatE2E?.captured ?? [],
|
|
snapshotPending: mainGlobal.__makelorePiFirstChatE2E?.snapshotPending ?? false,
|
|
});
|
|
});
|
|
}
|
|
|
|
async function releaseSnapshot(electronApp: ElectronApplication): Promise<void> {
|
|
await electronApp.evaluate(() => {
|
|
const mainGlobal = globalThis as typeof globalThis & {
|
|
__makelorePiFirstChatE2E?: { releaseSnapshot: (() => void) | null };
|
|
};
|
|
mainGlobal.__makelorePiFirstChatE2E?.releaseSnapshot?.();
|
|
});
|
|
}
|
|
|
|
test('first PI Conversation is editable under 500 ms and submits before runtime Snapshot', 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);
|
|
await disableCodingEventSource(page);
|
|
|
|
try {
|
|
await page.reload();
|
|
page = await getStableWindow(electronApp);
|
|
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
|
await page.getByTestId('ai-module-option-programming').click();
|
|
await expect(page.getByTestId('main-layout')).toBeVisible();
|
|
await page.evaluate(() => {
|
|
performance.mark('pi-first-chat-start');
|
|
window.location.hash = '/opencode-chat';
|
|
});
|
|
const composer = page.getByRole('textbox');
|
|
await expect(composer).toBeEnabled();
|
|
const editableMs = await page.evaluate(() => (
|
|
performance.now() - performance.getEntriesByName('pi-first-chat-start').at(-1)!.startTime
|
|
));
|
|
expect(editableMs).toBeLessThan(500);
|
|
|
|
const pixelPng = Buffer.from(
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
|
'base64',
|
|
);
|
|
await page.getByTestId('coding-file-attachment-input').setInputFiles({
|
|
name: 'pixel.png',
|
|
mimeType: 'image/png',
|
|
buffer: pixelPng,
|
|
});
|
|
await composer.fill('Build the first PI scene');
|
|
await expect(page.getByRole('button', { name: '发送' })).toBeEnabled();
|
|
await page.getByTestId('coding-message-composer').evaluate(
|
|
(form: HTMLFormElement) => form.requestSubmit(),
|
|
);
|
|
|
|
await expect.poll(async () => {
|
|
const state = await readState(electronApp);
|
|
return {
|
|
snapshotPending: state.snapshotPending,
|
|
promptPosted: state.captured.some((request) => (
|
|
request.path === '/api/coding/conversations/conversation-pi-first-chat/prompt'
|
|
&& request.method === 'POST'
|
|
)),
|
|
};
|
|
}).toEqual({ snapshotPending: true, promptPosted: true });
|
|
await expect(
|
|
page.getByTestId('coding-conversation-timeline').getByText('Build the first PI scene'),
|
|
).toBeVisible();
|
|
await expect(page.getByRole('img', { name: '对话图片附件' })).toBeVisible();
|
|
await expect(page.getByText('1 条消息已被本地 Agent 接收。')).toBeVisible();
|
|
const state = await readState(electronApp);
|
|
const uploads = state.captured.filter((request) => (
|
|
request.path === '/api/coding/attachments' && request.method === 'POST'
|
|
));
|
|
expect(uploads).toHaveLength(1);
|
|
expect(uploads[0]).toMatchObject({
|
|
byteLength: pixelPng.byteLength,
|
|
contentType: 'image/png',
|
|
});
|
|
expect(state.captured.some((request) => (
|
|
/^\/api\/coding\/attachments\/[^/]+\/content$/.test(request.path)
|
|
&& request.method === 'GET'
|
|
))).toBe(true);
|
|
const prompt = state.captured.find((request) => (
|
|
request.path === '/api/coding/conversations/conversation-pi-first-chat/prompt'
|
|
));
|
|
expect(prompt?.body?.attachments).toEqual([
|
|
{ attachmentId: expect.any(String) },
|
|
]);
|
|
expect(JSON.stringify(state.captured)).not.toContain(pixelPng.toString('base64'));
|
|
} finally {
|
|
await releaseSnapshot(electronApp);
|
|
}
|
|
});
|
|
|
|
test('PI feature UI isolates Conversations and exposes queue, interaction, model, subagent, and project tools', 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);
|
|
|
|
try {
|
|
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 = '/opencode-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.getByText('允许继续?')).toBeVisible();
|
|
await page.getByRole('button', { name: '确认', exact: true }).click();
|
|
|
|
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 ');
|
|
|
|
await page.getByRole('button', { name: 'Second Conversation' }).first().click();
|
|
await expect(page.getByTestId('coding-conversation-header')).toContainText('Second Conversation');
|
|
await expect(page.getByRole('combobox', { name: '当前对话模型' })).toHaveValue(
|
|
JSON.stringify(['account-e2e', 'model-b']),
|
|
);
|
|
await expect(page.getByRole('textbox')).toHaveValue('');
|
|
|
|
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.endsWith('/changes'))).toBe(true);
|
|
expect(state.captured.every((request) => !request.path.includes('/api/opencode/share'))).toBe(true);
|
|
} finally {
|
|
await releaseSnapshot(electronApp);
|
|
}
|
|
});
|