Makelore 2.0 initial clean snapshot
This commit is contained in:
332
tests/e2e/opencode-slash-commands.spec.ts
Normal file
332
tests/e2e/opencode-slash-commands.spec.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
import type { ElectronApplication } from 'playwright-core';
|
||||
import { completeSetup, expect, test } from './fixtures/electron';
|
||||
|
||||
interface CapturedSlashRequest {
|
||||
path: string;
|
||||
method: string;
|
||||
body?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function readCapturedRequests(
|
||||
electronApp: ElectronApplication,
|
||||
): Promise<CapturedSlashRequest[]> {
|
||||
const requests = await electronApp.evaluate(() => {
|
||||
type MainCapturedSlashRequest = {
|
||||
path: string;
|
||||
method: string;
|
||||
body?: Record<string, unknown>;
|
||||
};
|
||||
type MainState = {
|
||||
captured: MainCapturedSlashRequest[];
|
||||
commandFailure: string | null;
|
||||
};
|
||||
const mainGlobal = globalThis as typeof globalThis & {
|
||||
__niancodeSlashE2EState?: MainState;
|
||||
};
|
||||
return structuredClone(
|
||||
mainGlobal.__niancodeSlashE2EState?.captured ?? [],
|
||||
);
|
||||
});
|
||||
return requests.filter((request) => request.method !== 'GET');
|
||||
}
|
||||
|
||||
async function setSlashCommandFailure(
|
||||
electronApp: ElectronApplication,
|
||||
message: string | null,
|
||||
): Promise<void> {
|
||||
await electronApp.evaluate((value) => {
|
||||
type MainCapturedSlashRequest = {
|
||||
path: string;
|
||||
method: string;
|
||||
body?: Record<string, unknown>;
|
||||
};
|
||||
type MainState = {
|
||||
captured: MainCapturedSlashRequest[];
|
||||
commandFailure: string | null;
|
||||
};
|
||||
const mainGlobal = globalThis as typeof globalThis & {
|
||||
__niancodeSlashE2EState?: MainState;
|
||||
};
|
||||
if (!mainGlobal.__niancodeSlashE2EState) {
|
||||
throw new Error('Slash E2E Main state is unavailable');
|
||||
}
|
||||
mainGlobal.__niancodeSlashE2EState.commandFailure = value;
|
||||
}, message);
|
||||
}
|
||||
|
||||
async function installSlashCommandHost(
|
||||
electronApp: ElectronApplication,
|
||||
): Promise<void> {
|
||||
await electronApp.evaluate(async () => {
|
||||
const { ipcMain } = process.mainModule!.require(
|
||||
'electron',
|
||||
) as typeof import('electron');
|
||||
type MainCapturedSlashRequest = {
|
||||
path: string;
|
||||
method: string;
|
||||
body?: Record<string, unknown>;
|
||||
};
|
||||
type MainState = {
|
||||
captured: MainCapturedSlashRequest[];
|
||||
commandFailure: string | null;
|
||||
};
|
||||
const mainGlobal = globalThis as typeof globalThis & {
|
||||
__niancodeSlashE2EState?: MainState;
|
||||
};
|
||||
const state: MainState = {
|
||||
captured: [],
|
||||
commandFailure: null,
|
||||
};
|
||||
mainGlobal.__niancodeSlashE2EState = state;
|
||||
const project = {
|
||||
id: 'prj_slash_e2e',
|
||||
path: 'D:/e2e/slash',
|
||||
name: 'slash',
|
||||
createdAt: '2026-07-18T00:00:00.000Z',
|
||||
updatedAt: '2026-07-18T00:00:00.000Z',
|
||||
lastOpenedAt: '2026-07-18T00:00:00.000Z',
|
||||
};
|
||||
const session = {
|
||||
id: 'ses_slash_e2e',
|
||||
title: 'Slash E2E',
|
||||
agent: 'game-development',
|
||||
};
|
||||
const status = {
|
||||
state: 'running',
|
||||
port: 4096,
|
||||
url: 'http://127.0.0.1:4096',
|
||||
};
|
||||
const respond = (json: unknown, responseStatus = 200) => ({
|
||||
ok: true,
|
||||
data: {
|
||||
status: responseStatus,
|
||||
ok: responseStatus >= 200 && responseStatus < 300,
|
||||
json,
|
||||
},
|
||||
});
|
||||
|
||||
ipcMain.removeHandler('hostapi:fetch');
|
||||
ipcMain.handle('hostapi:fetch', async (
|
||||
_event,
|
||||
request: {
|
||||
path?: string;
|
||||
method?: string;
|
||||
body?: string | null;
|
||||
},
|
||||
) => {
|
||||
const path = request.path ?? '';
|
||||
const method = request.method ?? 'GET';
|
||||
const body = request.body
|
||||
? JSON.parse(request.body) as Record<string, unknown>
|
||||
: undefined;
|
||||
state.captured.push({
|
||||
path,
|
||||
method,
|
||||
...(body ? { body } : {}),
|
||||
});
|
||||
|
||||
if (path === '/api/opencode/status') return respond(status);
|
||||
if (path === '/api/opencode/health') {
|
||||
return respond({ ok: true, status });
|
||||
}
|
||||
if (
|
||||
path === '/api/opencode/projects'
|
||||
|| path.startsWith('/api/opencode/projects?')
|
||||
) {
|
||||
return respond({
|
||||
projects: [project],
|
||||
activeProject: project,
|
||||
});
|
||||
}
|
||||
if (
|
||||
path === '/api/opencode/projects/active'
|
||||
&& method === 'GET'
|
||||
) {
|
||||
return respond({
|
||||
projects: [project],
|
||||
activeProject: project,
|
||||
});
|
||||
}
|
||||
if (path.startsWith('/api/opencode/projects/config?')) {
|
||||
return respond({
|
||||
status: 'missing',
|
||||
knowledgeFiles: [],
|
||||
});
|
||||
}
|
||||
if (path.startsWith('/api/opencode/projects/template?')) {
|
||||
return respond({ status: 'missing' });
|
||||
}
|
||||
if (path === '/api/opencode/config-summary') {
|
||||
return respond({
|
||||
model: 'niancode-user-models/qwen3.7-plus',
|
||||
smallModel: null,
|
||||
providerIds: ['niancode-user-models'],
|
||||
enabledProviderIds: ['niancode-user-models'],
|
||||
providerCount: 1,
|
||||
});
|
||||
}
|
||||
if (path === '/api/provider-accounts') {
|
||||
return respond([]);
|
||||
}
|
||||
if (path === '/api/provider-accounts/key-info') {
|
||||
return respond([]);
|
||||
}
|
||||
if (path === '/api/provider-vendors') {
|
||||
return respond([]);
|
||||
}
|
||||
if (path === '/api/provider-accounts/default') {
|
||||
return respond({ accountId: null });
|
||||
}
|
||||
if (path === '/api/opencode/sessions') {
|
||||
return respond({ sessions: [session] });
|
||||
}
|
||||
if (path === '/api/opencode/sessions/status') {
|
||||
return respond({
|
||||
statuses: {
|
||||
ses_slash_e2e: { type: 'idle' },
|
||||
},
|
||||
});
|
||||
}
|
||||
if (
|
||||
path
|
||||
=== '/api/opencode/sessions/ses_slash_e2e/messages'
|
||||
&& method === 'GET'
|
||||
) {
|
||||
return respond({ messages: [] });
|
||||
}
|
||||
if (
|
||||
path
|
||||
=== '/api/opencode/sessions/ses_slash_e2e/todos'
|
||||
) {
|
||||
return respond({ todos: [] });
|
||||
}
|
||||
if (
|
||||
path
|
||||
=== '/api/opencode/sessions/ses_slash_e2e/diff'
|
||||
) {
|
||||
return respond({ diffs: [] });
|
||||
}
|
||||
if (path === '/api/opencode/questions') {
|
||||
return respond({ questions: [] });
|
||||
}
|
||||
if (path === '/api/opencode/permissions') {
|
||||
return respond({ permissions: [] });
|
||||
}
|
||||
if (path === '/api/opencode/files/status') {
|
||||
return respond({ files: [] });
|
||||
}
|
||||
if (path === '/api/opencode/commands') {
|
||||
return respond({
|
||||
commands: [{
|
||||
name: 'Review',
|
||||
hints: ['$ARGUMENTS'],
|
||||
}],
|
||||
shareEnabled: true,
|
||||
});
|
||||
}
|
||||
if (
|
||||
path
|
||||
=== '/api/opencode/sessions/ses_slash_e2e/summarize'
|
||||
&& method === 'POST'
|
||||
) {
|
||||
return respond({ success: true }, 202);
|
||||
}
|
||||
if (
|
||||
path
|
||||
=== '/api/opencode/sessions/ses_slash_e2e/command'
|
||||
&& method === 'POST'
|
||||
) {
|
||||
return state.commandFailure
|
||||
? respond(
|
||||
{
|
||||
success: false,
|
||||
error: state.commandFailure,
|
||||
},
|
||||
500,
|
||||
)
|
||||
: respond({ success: true }, 202);
|
||||
}
|
||||
throw new Error(
|
||||
`Unexpected hostapi request: ${method} ${path}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.describe('OpenCode slash commands', () => {
|
||||
test.afterEach(async ({ electronApp }) => {
|
||||
await electronApp.evaluate(async () => {
|
||||
const mainGlobal = globalThis as typeof globalThis & {
|
||||
__niancodeSlashE2EState?: unknown;
|
||||
};
|
||||
delete mainGlobal.__niancodeSlashE2EState;
|
||||
});
|
||||
});
|
||||
|
||||
test('selects on first Enter, executes on second Enter, blocks unknown commands, and preserves failures', async ({
|
||||
electronApp,
|
||||
page,
|
||||
}) => {
|
||||
await completeSetup(page);
|
||||
await installSlashCommandHost(electronApp);
|
||||
await page.reload();
|
||||
await page.getByTestId('sidebar-module-programming').click();
|
||||
await expect(page).toHaveURL(/\/opencode-chat$/);
|
||||
const composer = page.getByRole('textbox');
|
||||
await expect(composer).toBeVisible();
|
||||
|
||||
await composer.fill('/comp');
|
||||
await composer.press('Enter');
|
||||
await expect(composer).toHaveValue('/compact');
|
||||
await expect(
|
||||
page.getByText('再次按 Enter 执行'),
|
||||
).toBeVisible();
|
||||
expect(await readCapturedRequests(electronApp)).toEqual([]);
|
||||
|
||||
await composer.press('Enter');
|
||||
await expect(composer).toHaveValue('');
|
||||
await expect.poll(async () => (
|
||||
await readCapturedRequests(electronApp)
|
||||
).some((request) => (
|
||||
request.path.endsWith('/summarize')
|
||||
))).toBe(true);
|
||||
|
||||
await composer.fill('/unknown do-not-send');
|
||||
await composer.press('Enter');
|
||||
await expect(
|
||||
page.getByText('未知命令 /unknown'),
|
||||
).toBeVisible();
|
||||
await page
|
||||
.getByTestId('opencode-message-composer')
|
||||
.evaluate((form: HTMLFormElement) => form.requestSubmit());
|
||||
expect((
|
||||
await readCapturedRequests(electronApp)
|
||||
).some((request) => (
|
||||
request.path.endsWith('/messages')
|
||||
&& request.method === 'POST'
|
||||
))).toBe(false);
|
||||
await expect(composer).toHaveValue(
|
||||
'/unknown do-not-send',
|
||||
);
|
||||
|
||||
await setSlashCommandFailure(electronApp, 'review failed');
|
||||
await composer.fill('/Review staged changes ');
|
||||
await composer.press('Enter');
|
||||
await expect(page.getByText('review failed')).toBeVisible();
|
||||
await expect(composer).toHaveValue(
|
||||
'/Review staged changes ',
|
||||
);
|
||||
|
||||
await setSlashCommandFailure(electronApp, null);
|
||||
await composer.press('Enter');
|
||||
await expect(composer).toHaveValue('');
|
||||
const command = (await readCapturedRequests(electronApp))
|
||||
.findLast((request) => request.path.endsWith('/command'));
|
||||
expect(command?.body).toMatchObject({
|
||||
command: 'Review',
|
||||
arguments: ' staged changes ',
|
||||
agent: 'game-development',
|
||||
model: 'niancode-user-models/qwen3.7-plus',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user