import type { ElectronApplication } from 'playwright-core'; import { completeSetup, expect, test } from './fixtures/electron'; interface CapturedSlashRequest { path: string; method: string; body?: Record; } async function readCapturedRequests( electronApp: ElectronApplication, ): Promise { const requests = await electronApp.evaluate(() => { type MainCapturedSlashRequest = { path: string; method: string; body?: Record; }; 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 { await electronApp.evaluate((value) => { type MainCapturedSlashRequest = { path: string; method: string; body?: Record; }; 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 { await electronApp.evaluate(async () => { const { ipcMain } = process.mainModule!.require( 'electron', ) as typeof import('electron'); type MainCapturedSlashRequest = { path: string; method: string; body?: Record; }; 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 : 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', }); }); });