Makelore 2.0 initial clean snapshot

This commit is contained in:
inman
2026-07-29 17:22:35 +08:00
commit b8ca3f8eea
694 changed files with 139782 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
import { closeElectronApp, expect, test } from './fixtures/electron';
test.describe('NianCode Electron smoke flows', () => {
test('shows the setup wizard on a fresh profile', async ({ page }) => {
await expect(page.getByTestId('setup-page')).toBeVisible();
await expect(page.getByTestId('setup-welcome-step')).toBeVisible();
await expect(page.getByTestId('setup-skip-button')).toBeVisible();
});
test('can skip setup and continue to the login page', async ({ page }) => {
await expect(page.getByTestId('setup-page')).toBeVisible();
await page.getByTestId('setup-skip-button').click();
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
await expect(page.getByTestId('main-layout')).toHaveCount(0);
});
test('persists skipped setup across relaunch for the same isolated profile', async ({ electronApp, launchElectronApp }) => {
const firstWindow = await electronApp.firstWindow();
await firstWindow.waitForLoadState('domcontentloaded');
await firstWindow.getByTestId('setup-skip-button').click();
await expect(firstWindow.getByRole('button', { name: 'Sign in' })).toBeVisible();
await closeElectronApp(electronApp);
const relaunchedApp = await launchElectronApp();
try {
const relaunchedWindow = await relaunchedApp.firstWindow();
await relaunchedWindow.waitForLoadState('domcontentloaded');
await expect(relaunchedWindow.getByRole('button', { name: 'Sign in' })).toBeVisible();
await expect(relaunchedWindow.getByTestId('setup-page')).toHaveCount(0);
} finally {
await closeElectronApp(relaunchedApp);
}
});
});

View File

@@ -0,0 +1,94 @@
import { completeSetup, expect, test } from './fixtures/electron';
const testConfigResponses = {
channelsAccounts: {
success: true,
channels: [
{
channelType: 'feishu',
defaultAccountId: 'default',
status: 'connected',
accounts: [
{
accountId: 'default',
name: 'Primary Account',
configured: true,
status: 'connected',
isDefault: true,
},
],
},
],
},
agents: {
success: true,
agents: [],
},
credentialsValidate: {
success: true,
valid: true,
warnings: [],
},
channelConfig: {
success: true,
},
};
test.describe('Channels account ID validation', () => {
test('rejects non-canonical custom account ID before save', async ({ electronApp, page }) => {
await electronApp.evaluate(({ ipcMain }, responses) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).__niancodeE2eChannelConfigSaveCount = 0;
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string; method?: string }) => {
const method = request?.method ?? 'GET';
const path = request?.path ?? '';
if (path === '/api/channels/accounts' && method === 'GET') {
return { ok: true, data: { status: 200, ok: true, json: responses.channelsAccounts } };
}
if (path === '/api/agents' && method === 'GET') {
return { ok: true, data: { status: 200, ok: true, json: responses.agents } };
}
if (path === '/api/channels/credentials/validate' && method === 'POST') {
return { ok: true, data: { status: 200, ok: true, json: responses.credentialsValidate } };
}
if (path === '/api/channels/config' && method === 'POST') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).__niancodeE2eChannelConfigSaveCount += 1;
return { ok: true, data: { status: 200, ok: true, json: responses.channelConfig } };
}
if (path.startsWith('/api/channels/config/') && method === 'GET') {
return { ok: true, data: { status: 200, ok: true, json: { success: true, values: {} } } };
}
return {
ok: false,
error: { message: `Unexpected hostapi:fetch request: ${method} ${path}` },
};
});
}, testConfigResponses);
await completeSetup(page);
await page.getByTestId('sidebar-nav-channels').click();
await expect(page.getByTestId('channels-page')).toBeVisible();
await expect(page.getByText('Feishu / Lark')).toBeVisible();
await page.getByRole('button', { name: /Add Account|account\.add/i }).click();
await expect(page.getByText(/Configure Feishu \/ Lark|dialog\.configureTitle/)).toBeVisible();
await page.locator('#account-id').fill('测试账号');
await page.locator('#appId').fill('cli_test');
await page.locator('#appSecret').fill('secret_test');
await page.getByRole('button', { name: /Save & Connect|dialog\.saveAndConnect/ }).click();
await expect(page.getByText(/account\.invalidCanonicalId|must use lowercase letters/i).first()).toBeVisible();
const saveCalls = await electronApp.evaluate(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const count = Number((globalThis as any).__niancodeE2eChannelConfigSaveCount || 0);
return { count };
});
expect(saveCalls.count).toBe(0);
});
});

View File

@@ -0,0 +1,133 @@
import { completeSetup, expect, test } from './fixtures/electron';
test.describe('Channels binding regression', () => {
test('keeps newly added non-default Feishu accounts unassigned until the user binds an agent', async ({ electronApp, page }) => {
await electronApp.evaluate(({ ipcMain }) => {
const state = {
nextAccountId: 'feishu-a1b2c3d4',
saveCount: 0,
bindingCount: 0,
channels: [
{
channelType: 'feishu',
defaultAccountId: 'default',
status: 'connected',
accounts: [
{
accountId: 'default',
name: 'Primary Account',
configured: true,
status: 'connected',
isDefault: true,
agentId: 'main',
},
],
},
],
agents: [
{ id: 'main', name: 'Main Agent' },
{ id: 'code', name: 'Code Agent' },
],
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).__niancodeE2eBindingRegression = state;
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string; method?: string; body?: string }) => {
const method = request?.method ?? 'GET';
const path = request?.path ?? '';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const current = (globalThis as any).__niancodeE2eBindingRegression as typeof state;
if (path === '/api/channels/accounts' && method === 'GET') {
return { ok: true, data: { status: 200, ok: true, json: { success: true, channels: current.channels } } };
}
if (path === '/api/agents' && method === 'GET') {
return { ok: true, data: { status: 200, ok: true, json: { success: true, agents: current.agents } } };
}
if (path === '/api/channels/credentials/validate' && method === 'POST') {
return { ok: true, data: { status: 200, ok: true, json: { success: true, valid: true, warnings: [] } } };
}
if (path === '/api/channels/config' && method === 'POST') {
current.saveCount += 1;
const body = JSON.parse(request?.body ?? '{}') as { accountId?: string };
const accountId = body.accountId || current.nextAccountId;
const feishu = current.channels[0];
if (!feishu.accounts.some((account) => account.accountId === accountId)) {
feishu.accounts.push({
accountId,
name: accountId,
configured: true,
status: 'connected',
isDefault: false,
});
}
return { ok: true, data: { status: 200, ok: true, json: { success: true } } };
}
if (path === '/api/channels/binding' && method === 'PUT') {
current.bindingCount += 1;
const body = JSON.parse(request?.body ?? '{}') as { channelType?: string; accountId?: string; agentId?: string };
if (body.channelType === 'feishu' && body.accountId) {
const feishu = current.channels[0];
const account = feishu.accounts.find((entry) => entry.accountId === body.accountId);
if (account) {
account.agentId = body.agentId;
}
}
return { ok: true, data: { status: 200, ok: true, json: { success: true } } };
}
if (path === '/api/channels/binding' && method === 'DELETE') {
current.bindingCount += 1;
return { ok: true, data: { status: 200, ok: true, json: { success: true } } };
}
if (path.startsWith('/api/channels/config/') && method === 'GET') {
return { ok: true, data: { status: 200, ok: true, json: { success: true, values: {} } } };
}
return {
ok: false,
error: { message: `Unexpected hostapi:fetch request: ${method} ${path}` },
};
});
});
await completeSetup(page);
await page.getByTestId('sidebar-nav-channels').click();
await expect(page.getByTestId('channels-page')).toBeVisible();
await expect(page.getByText('Feishu / Lark')).toBeVisible();
const feishuGroupHeader = page.locator('div.rounded-2xl').filter({ hasText: 'Feishu / Lark' }).first();
await expect(feishuGroupHeader).toContainText(/Connected|已连接|接続済み|Подключён/);
await page.getByRole('button', { name: /Add Account|添加账号|アカウントを追加/ }).click();
await expect(page.getByText(/Configure Feishu \/ Lark|dialog\.configureTitle/)).toBeVisible();
const accountIdInput = page.locator('#account-id');
const newAccountId = await accountIdInput.inputValue();
await expect(accountIdInput).toHaveValue(/feishu-/);
await page.locator('#appId').fill('cli_test');
await page.locator('#appSecret').fill('secret_test');
await page.getByRole('button', { name: /Save & Connect|dialog\.saveAndConnect/ }).click();
await expect(page.getByText(/Configure Feishu \/ Lark|dialog\.configureTitle/)).toBeHidden();
const newAccountRow = page.locator('div.rounded-xl').filter({ hasText: newAccountId }).first();
await expect(newAccountRow).toBeVisible();
const bindingSelect = newAccountRow.locator('select');
await expect(bindingSelect).toHaveValue('');
await bindingSelect.selectOption('code');
await expect(bindingSelect).toHaveValue('code');
const counters = await electronApp.evaluate(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const state = (globalThis as any).__niancodeE2eBindingRegression as { saveCount: number; bindingCount: number };
return { saveCount: state.saveCount, bindingCount: state.bindingCount };
});
expect(counters.saveCount).toBe(1);
expect(counters.bindingCount).toBe(1);
});
});

View File

@@ -0,0 +1,104 @@
import type { Page } from '@playwright/test';
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
async function openSettingsFromSidebarAccountMenu(page: Page): Promise<void> {
await page.getByTestId('sidebar-member-menu-trigger').click();
await page.getByTestId('sidebar-nav-settings').click();
}
async function expectSidebarSettingsLabel(page: Page, label: string): Promise<void> {
await page.getByTestId('sidebar-member-menu-trigger').click();
await expect(page.getByTestId('sidebar-nav-settings')).toContainText(label);
await page.keyboard.press('Escape');
}
test.describe('Russian language localization', () => {
test('shows Russian language option in setup wizard', async ({ launchElectronApp }) => {
const app = await launchElectronApp();
try {
const page = await getStableWindow(app);
// Should see the setup wizard
await expect(page.getByTestId('setup-page')).toBeVisible();
// Should have Russian language button visible
const russianButton = page.locator('button', { hasText: 'Русский' });
await expect(russianButton).toBeVisible();
} finally {
await closeElectronApp(app);
}
});
test('can switch to Russian language in setup wizard', async ({ launchElectronApp }) => {
const app = await launchElectronApp();
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('setup-page')).toBeVisible();
// Click Russian language button
const russianButton = page.locator('button', { hasText: 'Русский' });
await russianButton.click();
// Verify UI renders in Russian by checking for Russian-only text
// "Добро пожаловать" is unique to Russian and won't appear in English
await expect(page.locator('h2')).toContainText('Добро пожаловать');
} finally {
await closeElectronApp(app);
}
});
test('Russian language persists after skipping setup', async ({ launchElectronApp }) => {
const app = await launchElectronApp();
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('setup-page')).toBeVisible();
// Switch to Russian
const russianButton = page.locator('button', { hasText: 'Русский' });
await russianButton.click();
// Skip setup
await page.getByTestId('setup-skip-button').click();
await expect(page.getByTestId('main-layout')).toBeVisible();
// Navigate to Settings to verify language persistence
await openSettingsFromSidebarAccountMenu(page);
await expect(page.getByTestId('settings-page')).toBeVisible();
// Verify sidebar shows Russian text (not English)
// "Настройки" is Russian-only, English is "Settings"
await expectSidebarSettingsLabel(page, 'Настройки');
} finally {
await closeElectronApp(app);
}
});
test('can switch to Russian in Settings page', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('main-layout')).toBeVisible();
// Navigate to Settings (in English by default after skipSetup)
await openSettingsFromSidebarAccountMenu(page);
await expect(page.getByTestId('settings-page')).toBeVisible();
// Click Russian language button
const russianButton = page.locator('button', { hasText: 'Русский' });
await russianButton.click();
// Verify sidebar switched to Russian
// "Настройки" is Russian-only, English is "Settings"
await expectSidebarSettingsLabel(page, 'Настройки');
} finally {
await closeElectronApp(app);
}
});
});

View File

@@ -0,0 +1,77 @@
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
test.describe('NianCode main navigation without setup flow', () => {
test('navigates between core pages with setup bypassed', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
const minimumSize = await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.getMinimumSize());
expect(minimumSize).toEqual([1100, 700]);
await expect(page.getByTestId('main-layout')).toBeVisible();
await expect(page.getByText('请先新建项目')).toBeVisible();
await expect(page.getByText('一念成光,万物可创。')).toBeVisible();
await expect(page.getByRole('img', { name: 'Makelore logo' })).toBeVisible();
await expect(page.getByText('Makelore 工作台', { exact: true })).toBeVisible();
await expect(page.getByText(/NITU/)).toHaveCount(0);
await expect(page.getByTestId('sidebar-create-project')).toBeVisible();
await expect(page.getByTestId('sidebar-nav-publish')).toHaveCount(0);
await expect(page.getByTestId('sidebar-nav-project-config')).toHaveCount(0);
await expect(page.getByTestId('sidebar-nav-agent-chat')).toHaveCount(0);
await expect(page.getByRole('button', { name: '添加已有项目' })).toHaveCount(0);
await expect(page.getByTestId('sidebar-nav-character')).toHaveText('角色场景');
await expect(page.getByText('成长成就在这里')).toHaveCount(0);
await expect(page.getByText('已创建 Phaser 2D 小游戏起步工程')).toHaveCount(0);
await expect(page.getByTestId('game-asset-browser')).toHaveCount(0);
await expect(page.getByTestId('game-asset-review-summary')).toHaveCount(0);
await page.getByTestId('resource-card-models').click();
await expect(page.getByTestId('nitu-model-config-refresh-button')).toBeVisible();
await page.keyboard.press('Escape');
await page.getByTestId('resource-card-skills').click();
await expect(page.getByRole('heading', { name: '可用技能库' })).toBeVisible();
await expect(page.getByTestId('skill-library-card-youth-plain-language')).toContainText('青少年通俗表达');
await expect(page.getByTestId('skill-library-card-youth-plain-language')).not.toContainText('youth-plain-language');
await expect(page.getByTestId('skill-library-card-pm-project-plan')).toContainText('通用软件项目规划');
await expect(page.getByTestId('skill-library-card-pm-project-plan')).not.toContainText('pm-project-plan');
await page.keyboard.press('Escape');
await page.getByTestId('sidebar-nav-models').click();
await expect(page.getByTestId('models-page')).toBeVisible();
await expect(page.getByTestId('models-page-title')).toBeVisible();
await expect(page.getByTestId('sidebar-module-switcher')).toBeVisible();
await expect(page.getByTestId('sidebar-module-switcher-trigger')).toHaveAttribute('aria-expanded', 'false');
await page.getByTestId('sidebar-module-switcher-trigger').click();
await expect(page.getByTestId('sidebar-module-programming')).toHaveAttribute('aria-current', 'page');
await page.getByTestId('sidebar-module-painting').click();
await expect(page.getByTestId('image-canvas-page')).toBeVisible();
await expect(page.getByTestId('image-workspace-unavailable')).toBeVisible();
await expect(page.getByText('创作空间暂不可用').first()).toBeVisible();
await expect(page.getByTestId('sidebar-image-workspace')).toBeVisible();
await expect(page.getByTestId('sidebar-create-image-project')).toBeVisible();
await expect(page.getByText('本地开发')).toHaveCount(0);
await expect(page.getByTestId('sidebar-reset-local-image-workspace')).toHaveCount(0);
await expect(page.getByTestId('sidebar-create-project')).toHaveCount(0);
await expect(page.getByText('制作中心')).toHaveCount(0);
await expect(page.getByText('任务模块')).toHaveCount(0);
await expect(page.getByTestId('sidebar-module-painting')).toHaveAttribute('aria-current', 'page');
await page.getByTestId('sidebar-module-switcher-trigger').click();
await page.getByTestId('sidebar-module-programming').click();
await expect(page.getByText('请先新建项目')).toBeVisible();
await expect(page.getByTestId('chat-operation-page')).toHaveCount(0);
await expect(page.getByTestId('sidebar-nav-asset-square')).toHaveCount(0);
await page.getByTestId('sidebar-nav-agents').click();
await expect(page.getByTestId('agents-page')).toBeVisible();
await page.getByTestId('sidebar-nav-channels').click();
await expect(page.getByTestId('channels-page')).toBeVisible();
} finally {
await closeElectronApp(app);
}
});
});

View File

@@ -0,0 +1,132 @@
import sharp from 'sharp';
import {
closeElectronApp,
expect,
getStableWindow,
test,
} from './fixtures/electron';
test.describe('OpenCode image compression composer', () => {
test('preprocesses a 4K image and lets the user switch variants', async ({
launchElectronApp,
}) => {
const app = await launchElectronApp({ skipSetup: true });
try {
await app.evaluate(async () => {
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');
const activeProject = {
id: 'prj_e2e_compression',
path: 'D:/e2e/image-compression',
name: 'image-compression',
createdAt: '2026-07-18T00:00:00.000Z',
updatedAt: '2026-07-18T00:00:00.000Z',
lastOpenedAt: '2026-07-18T00:00:00.000Z',
};
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: unknown,
request: { path?: string; method?: string },
) => {
const path = request.path ?? '';
const method = request.method ?? 'GET';
if (path === '/api/opencode/status') {
return respond({
state: 'running',
port: 4096,
url: 'http://127.0.0.1:4096',
});
}
if (path.startsWith('/api/opencode/projects')) {
return respond({ projects: [activeProject], activeProject });
}
if (path === '/api/opencode/config-summary') {
return respond({
model: 'niancode-user-models/qwen3.7-plus',
smallModel: null,
providerIds: ['niancode-user-models'],
providerCount: 1,
});
}
if (path === '/api/opencode/sessions') {
return respond({
sessions: [{
id: 'ses_e2e_compression',
title: 'Image compression',
updatedAt: '2026-07-18T00:00:00.000Z',
}],
});
}
if (path === '/api/opencode/sessions/status') {
return respond({
statuses: { ses_e2e_compression: { type: 'idle' } },
});
}
if (
path === '/api/opencode/sessions/ses_e2e_compression/messages'
&& method === 'GET'
) {
return respond({ messages: [] });
}
if (path.endsWith('/todos')) return respond({ todos: [] });
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 });
}
return respond({});
});
});
const page = await getStableWindow(app);
await page.reload();
await expect(page.getByTestId('sidebar-nav-agent-chat')).toBeVisible();
await page.getByTestId('sidebar-nav-agent-chat').click();
await expect(page.getByTestId('opencode-message-composer')).toBeVisible();
const png = await sharp({
create: {
width: 3840,
height: 2160,
channels: 4,
background: { r: 64, g: 210, b: 200, alpha: 1 },
},
}).png().toBuffer();
await page.getByTestId('opencode-file-attachment-input').setInputFiles({
name: 'four-k.png',
mimeType: 'image/png',
buffer: png,
});
const card = page.getByTestId('opencode-composer-attachment');
await expect(card).toContainText('3840×2160');
await expect(card).toContainText('1930×1086');
await expect(page.getByTestId('opencode-attachment-use-compressed'))
.toHaveAttribute('aria-pressed', 'true');
await expect(page.getByTestId('opencode-attachment-token-estimate'))
.toContainText('约2049 Token');
await page.getByTestId('opencode-attachment-use-original').click();
await expect(page.getByTestId('opencode-attachment-use-original'))
.toHaveAttribute('aria-pressed', 'true');
await expect(page.getByTestId('opencode-attachment-token-estimate'))
.toContainText('约8102 Token');
await page.getByTestId('opencode-attachment-use-compressed').click();
await expect(page.getByTestId('opencode-attachment-use-compressed'))
.toHaveAttribute('aria-pressed', 'true');
await page.getByRole('button', { name: '移除附件 four-k.png' }).click();
await expect(card).toHaveCount(0);
} finally {
await closeElectronApp(app);
}
});
});

View 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',
});
});
});

View File

@@ -0,0 +1,37 @@
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
test.describe('Project-level Superpowers setting', () => {
test('starts game projects with Superpowers disabled', async ({ launchElectronApp }) => {
const parentPath = await mkdtemp(path.join(tmpdir(), 'niancode-superpowers-e2e-'));
const app = await launchElectronApp({ skipSetup: true });
try {
await app.evaluate(({ ipcMain }, selectedPath) => {
ipcMain.removeHandler('dialog:open');
ipcMain.handle('dialog:open', async () => ({ canceled: false, filePaths: [selectedPath] }));
}, parentPath);
const page = await getStableWindow(app);
await page.getByTestId('sidebar-create-project').click();
await expect(page.getByRole('radio', { name: '直接使用所选文件夹(默认)' })).toBeChecked();
await page.getByRole('button', { name: '选择路径' }).click();
await expect(page.getByLabel('项目路径')).toHaveValue(parentPath);
await page.getByTestId('project-template-option-game-development').click();
await page.getByRole('button', { name: '确认创建' }).click();
await expect(page.getByTestId('sidebar-nav-project-config')).toBeVisible();
await expect(page.getByTestId('sidebar-nav-publish')).toHaveCount(0);
await page.getByTestId('resource-card-skills').click();
await expect(page.getByRole('switch', { name: '启用 Superpowers' })).toHaveAttribute('aria-checked', 'false');
await expect(page.getByText('已关闭')).toBeVisible();
await page.getByRole('button', { name: '关闭' }).click();
await expect(page.getByTestId('resource-card-publish')).toHaveCount(0);
} finally {
await closeElectronApp(app);
await rm(parentPath, { recursive: true, force: true });
}
});
});

View File

@@ -0,0 +1,243 @@
import { completeSetup, expect, test } from './fixtures/electron';
const TEST_PROVIDER_ID = 'moonshot-e2e';
const TEST_PROVIDER_LABEL = 'Moonshot E2E';
async function seedTestProvider(page: Parameters<typeof completeSetup>[0]): Promise<void> {
await page.evaluate(async ({ providerId, providerLabel }) => {
const now = new Date().toISOString();
await window.electron.ipcRenderer.invoke('provider:save', {
id: providerId,
name: providerLabel,
type: 'moonshot',
baseUrl: 'https://api.moonshot.cn/v1',
model: 'kimi-k2.6',
enabled: true,
createdAt: now,
updatedAt: now,
});
}, { providerId: TEST_PROVIDER_ID, providerLabel: TEST_PROVIDER_LABEL });
}
test.describe('NianCode provider lifecycle', () => {
test('shows a saved provider and removes it cleanly after deletion', async ({ page }) => {
await completeSetup(page);
await seedTestProvider(page);
await page.getByTestId('sidebar-nav-models').click();
await expect(page.getByTestId('providers-settings')).toBeVisible();
await expect(page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toContainText(TEST_PROVIDER_LABEL);
await page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`).hover();
await page.getByTestId(`provider-delete-${TEST_PROVIDER_ID}`).click();
await expect(page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toHaveCount(0);
await expect(page.getByText(TEST_PROVIDER_LABEL)).toHaveCount(0);
});
test('does not redisplay a deleted provider after relaunch', async ({ electronApp, launchElectronApp, page }) => {
await completeSetup(page);
await seedTestProvider(page);
await page.getByTestId('sidebar-nav-models').click();
await expect(page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toContainText(TEST_PROVIDER_LABEL);
await page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`).hover();
await page.getByTestId(`provider-delete-${TEST_PROVIDER_ID}`).click();
await expect(page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toHaveCount(0);
await electronApp.close();
const relaunchedApp = await launchElectronApp();
try {
const relaunchedPage = await relaunchedApp.firstWindow();
await relaunchedPage.waitForLoadState('domcontentloaded');
await expect(relaunchedPage.getByTestId('main-layout')).toBeVisible();
await relaunchedPage.getByTestId('sidebar-nav-models').click();
await expect(relaunchedPage.getByTestId('providers-settings')).toBeVisible();
await expect(relaunchedPage.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toHaveCount(0);
await expect(relaunchedPage.getByText(TEST_PROVIDER_LABEL)).toHaveCount(0);
} finally {
await relaunchedApp.close();
}
});
test('trims whitespace before validating and saving a custom provider key', async ({ electronApp, page }) => {
await completeSetup(page);
await electronApp.evaluate(async ({ app: _app }) => {
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');
let accounts: Array<Record<string, unknown>> = [];
let keyInfo: Array<{ accountId: string; hasKey: boolean; keyMasked: string | null }> = [];
let statuses: Array<Record<string, unknown>> = [];
let defaultAccountId: string | null = null;
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: unknown, 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) : null;
// New account-based endpoints (preferred path).
if (path === '/api/provider-accounts' && method === 'GET') return respond(accounts);
if (path === '/api/provider-accounts/key-info' && method === 'GET') return respond(keyInfo);
if (path === '/api/provider-vendors' && method === 'GET') return respond([]);
if (path === '/api/provider-accounts/default' && method === 'GET') return respond({ accountId: defaultAccountId });
if (path === '/api/provider-accounts/validate' && method === 'POST') {
if (body?.apiKey !== 'sk-lm-test') {
return respond({ valid: false, error: `unexpected key: ${String(body?.apiKey)}` }, 400);
}
return respond({ valid: true });
}
if (path === '/api/provider-accounts' && method === 'POST') {
accounts = [body.account];
keyInfo = [{
accountId: body.account.id,
hasKey: Boolean(body.apiKey),
keyMasked: body.apiKey ? 'sk-***' : null,
}];
// Keep statuses populated for any consumer still on the legacy path.
statuses = [{
id: body.account.id,
name: body.account.label,
type: body.account.vendorId,
baseUrl: body.account.baseUrl,
model: body.account.model,
enabled: body.account.enabled,
createdAt: body.account.createdAt,
updatedAt: body.account.updatedAt,
hasKey: Boolean(body.apiKey),
keyMasked: body.apiKey ? 'sk-***' : null,
}];
return respond({ success: true });
}
if (path === '/api/provider-accounts/default' && method === 'PUT') {
defaultAccountId = body?.accountId ?? null;
return respond({ success: true });
}
// ── Legacy compatibility shims ─────────────────────────────
// Older renderer builds still reach for these. Keeping them
// wired up here exercises the backward-compat path in the
// route layer (it returns the same data, just without the
// newer key-info payload structure).
if (path === '/api/providers' && method === 'GET') return respond(statuses);
if (path === '/api/providers/validate' && method === 'POST') {
if (body?.apiKey !== 'sk-lm-test') {
return respond({ valid: false, error: `unexpected key: ${String(body?.apiKey)}` }, 400);
}
return respond({ valid: true });
}
return respond({});
});
});
await page.getByTestId('sidebar-nav-models').click();
await expect(page.getByTestId('providers-settings')).toBeVisible();
await page.getByTestId('providers-add-button').click();
await expect(page.getByTestId('add-provider-dialog')).toBeVisible();
await page.getByTestId('add-provider-type-custom').click();
await page.getByTestId('add-provider-name-input').fill('LM Studio Local');
await page.getByTestId('add-provider-api-key-input').fill(' sk-lm-test \n');
await page.getByTestId('add-provider-base-url-input').fill('http://127.0.0.1:1234/v1');
await page.getByTestId('add-provider-model-id-input').fill('local-model');
await page.getByTestId('add-provider-submit-button').click();
await expect(page.getByTestId('provider-card-custom')).toContainText('LM Studio Local');
});
test('edit form validates the new API key inline before saving (single button)', async ({ electronApp, page }) => {
await completeSetup(page);
await electronApp.evaluate(async ({ app: _app }) => {
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');
const provider = {
id: 'moonshot-edit',
vendorId: 'moonshot',
label: 'Moonshot Edit',
authMode: 'api_key',
baseUrl: 'https://api.moonshot.cn/v1',
model: 'kimi-k2.6',
enabled: true,
isDefault: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
let storedKey = 'sk-existing';
let keyInfo = [{ accountId: provider.id, hasKey: true, keyMasked: 'sk-***' }];
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: unknown, 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) : null;
if (path === '/api/provider-accounts' && method === 'GET') return respond([provider]);
if (path === '/api/provider-accounts/key-info' && method === 'GET') return respond(keyInfo);
if (path === '/api/provider-vendors' && method === 'GET') return respond([]);
if (path === '/api/provider-accounts/default' && method === 'GET') return respond({ accountId: provider.id });
if (path === '/api/provider-accounts/validate' && method === 'POST') {
if (body?.apiKey === 'sk-good') {
return respond({ valid: true });
}
return respond({ valid: false, error: 'Invalid API key' }, 400);
}
if (path.startsWith('/api/provider-accounts/') && method === 'PUT') {
if (body?.apiKey) storedKey = body.apiKey;
keyInfo = [{ accountId: provider.id, hasKey: Boolean(storedKey), keyMasked: 'sk-***' }];
return respond({ success: true });
}
if (path === '/api/providers' && method === 'GET') return respond([provider]);
return respond({});
});
});
await page.getByTestId('sidebar-nav-models').click();
await expect(page.getByTestId('providers-settings')).toBeVisible();
await expect(page.getByTestId('provider-card-moonshot-edit')).toBeVisible();
await page.getByTestId('provider-card-moonshot-edit').hover();
await page.getByTestId('provider-edit-moonshot-edit').click();
await page.getByTestId('provider-edit-key-input-moonshot-edit').fill('sk-bad');
await page.getByTestId('provider-edit-save-moonshot-edit').click();
await expect(page.getByTestId('provider-edit-validation-error-moonshot-edit')).toContainText('Invalid API key');
await page.getByTestId('provider-edit-key-input-moonshot-edit').fill('sk-good');
await expect(page.getByTestId('provider-edit-validation-error-moonshot-edit')).toHaveCount(0);
await page.getByTestId('provider-edit-save-moonshot-edit').click();
await expect(page.getByTestId('provider-edit-save-moonshot-edit')).toHaveCount(0);
});
});

View File

@@ -0,0 +1,44 @@
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
test.describe('hover-only scrollbar visibility', () => {
test('hides scrollbars until a scroll container is hovered', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await page.getByTestId('sidebar-nav-models').click();
await expect(page.getByTestId('models-page')).toBeVisible();
const scrollContainer = page.locator('[data-testid="models-page"] .overflow-y-auto').first();
await expect(scrollContainer).toBeVisible();
const beforeHover = await scrollContainer.evaluate((element) => {
const style = window.getComputedStyle(element);
const thumbStyle = window.getComputedStyle(element, '::-webkit-scrollbar-thumb');
return {
scrollbarWidth: style.scrollbarWidth,
thumbBackground: thumbStyle.backgroundColor,
};
});
await expect(scrollContainer).toHaveCSS('scrollbar-width', 'thin');
expect(beforeHover.thumbBackground).toBe('rgba(0, 0, 0, 0)');
await scrollContainer.hover();
const afterHover = await scrollContainer.evaluate((element) => {
const style = window.getComputedStyle(element);
const thumbStyle = window.getComputedStyle(element, '::-webkit-scrollbar-thumb');
return {
scrollbarWidth: style.scrollbarWidth,
thumbBackground: thumbStyle.backgroundColor,
};
});
expect(afterHover.scrollbarWidth).toBe('thin');
expect(afterHover.thumbBackground).not.toBe('rgba(0, 0, 0, 0)');
} finally {
await closeElectronApp(app);
}
});
});

View File

@@ -0,0 +1,54 @@
import type { Locator, Page } from '@playwright/test';
import { completeSetup, expect, test } from './fixtures/electron';
async function ensureSwitchState(toggle: Locator, checked: boolean): Promise<void> {
const currentState = await toggle.getAttribute('data-state');
const isChecked = currentState === 'checked';
if (isChecked !== checked) {
await toggle.click();
}
}
async function readProxyEnabled(page: Page): Promise<boolean> {
return await page.evaluate(async () => {
const settings = await window.electron.ipcRenderer.invoke('settings:getAll');
return Boolean(settings?.proxyEnabled);
});
}
async function openSettingsFromSidebarAccountMenu(page: Page): Promise<void> {
await page.getByTestId('sidebar-member-menu-trigger').click();
await page.getByTestId('sidebar-nav-settings').click();
}
test.describe('NianCode developer proxy settings', () => {
test('keeps proxy save available when disabling proxy in developer mode', async ({ page }) => {
await completeSetup(page);
await openSettingsFromSidebarAccountMenu(page);
await expect(page.getByTestId('settings-page')).toBeVisible();
const devModeToggle = page.getByTestId('settings-dev-mode-switch');
await expect(devModeToggle).toBeVisible();
await ensureSwitchState(devModeToggle, true);
const proxySection = page.getByTestId('settings-proxy-section');
const proxyToggle = page.getByTestId('settings-proxy-toggle');
const proxySaveButton = page.getByTestId('settings-proxy-save-button');
await expect(proxySection).toBeVisible();
await expect(proxyToggle).toBeVisible();
await expect(proxySaveButton).toBeVisible();
await ensureSwitchState(proxyToggle, true);
await expect(proxySaveButton).toBeEnabled();
await proxySaveButton.click();
await expect.poll(async () => await readProxyEnabled(page)).toBe(true);
await ensureSwitchState(proxyToggle, false);
await expect(proxySaveButton).toBeVisible();
await expect(proxySaveButton).toBeEnabled();
await proxySaveButton.click();
await expect.poll(async () => await readProxyEnabled(page)).toBe(false);
});
});

View File

@@ -0,0 +1,22 @@
import { completeSetup, expect, test } from './fixtures/electron';
test.describe('NianCode sidebar updates', () => {
test('shows an available update beside the lower-left account control', async ({ electronApp, page }) => {
await completeSetup(page);
const account = page.getByTestId('sidebar-member-menu-trigger');
const update = page.getByTestId('sidebar-update-button');
await expect(update).toBeHidden();
await electronApp.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0]?.webContents.send('update:status-changed', {
status: 'available',
info: { version: '9.9.9' },
});
});
await expect(update).toBeVisible();
await expect(update).toHaveAttribute('aria-label', '下载新版本 9.9.9');
await expect(account.locator('xpath=..').getByTestId('sidebar-update-button')).toBeVisible();
});
});

View File

@@ -0,0 +1,46 @@
import type { ElectronApplication } from '@playwright/test';
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
async function getZoomLevel(app: ElectronApplication): Promise<number> {
return await app.evaluate(({ BrowserWindow }) => {
const win = BrowserWindow.getAllWindows()[0];
return win?.webContents.getZoomLevel() ?? 0;
});
}
async function sendZoomShortcut(app: ElectronApplication, action: 'in' | 'out'): Promise<void> {
await app.evaluate(({ BrowserWindow }, zoomAction) => {
const win = BrowserWindow.getAllWindows()[0];
const contents = win?.webContents;
if (!contents) return;
const input = zoomAction === 'out'
? { key: '-', code: 'Minus', control: true, meta: false, alt: false }
: { key: '=', code: 'Equal', control: true, meta: false, alt: false };
contents.emit('before-input-event', { preventDefault() {} }, input);
}, action);
}
test.describe('NianCode window zoom shortcuts', () => {
test('can zoom back in after zooming out with keyboard shortcuts', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('main-layout')).toBeVisible();
await app.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0]?.webContents.setZoomLevel(0);
});
await sendZoomShortcut(app, 'out');
await expect.poll(async () => await getZoomLevel(app)).toBe(-1);
await sendZoomShortcut(app, 'in');
await expect.poll(async () => await getZoomLevel(app)).toBe(0);
} finally {
await closeElectronApp(app);
}
});
});