合并远程主分支客户端收口

This commit is contained in:
2026-08-13 17:13:36 +08:00
240 changed files with 4900 additions and 20399 deletions

View File

@@ -1,25 +1,43 @@
import { closeElectronApp, expect, test } from './fixtures/electron';
test.describe('Makelore Electron smoke flows', () => {
test('loads the bundled mixed-script UI font', async ({ page }) => {
await page.evaluate(async () => {
await document.fonts.load('400 16px "Makelore Latin"', 'Makelore');
await document.fonts.load('400 16px "Makelore CJK"', '中文');
await document.fonts.load('600 16px "Makelore Latin"', 'Makelore');
await document.fonts.load('600 16px "Makelore CJK"', '中文');
});
await expect.poll(async () => page.evaluate(() => getComputedStyle(document.body).fontFamily))
.toContain('Makelore Latin');
await expect.poll(async () => page.evaluate(() => (
Array.from(document.fonts).some((font) => font.family === 'Makelore CJK' && font.status === 'loaded')
))).toBe(true);
});
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 }) => {
test('can skip setup and open the public module chooser', async ({ page }) => {
await expect(page.getByTestId('setup-page')).toBeVisible();
await page.getByTestId('setup-skip-button').click();
await expect(page.getByRole('button', { name: 'Continue in browser' })).toBeVisible();
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await expect(page.getByTestId('main-layout')).toHaveCount(0);
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByRole('button', { name: '在浏览器中继续' })).toBeVisible();
});
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: 'Continue in browser' })).toBeVisible();
await expect(firstWindow.getByTestId('ai-module-selection-page')).toBeVisible();
await closeElectronApp(electronApp);
@@ -28,7 +46,7 @@ test.describe('Makelore Electron smoke flows', () => {
const relaunchedWindow = await relaunchedApp.firstWindow();
await relaunchedWindow.waitForLoadState('domcontentloaded');
await expect(relaunchedWindow.getByRole('button', { name: 'Continue in browser' })).toBeVisible();
await expect(relaunchedWindow.getByTestId('ai-module-selection-page')).toBeVisible();
await expect(relaunchedWindow.getByTestId('setup-page')).toHaveCount(0);
} finally {
await closeElectronApp(relaunchedApp);

View File

@@ -1,94 +0,0 @@
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

@@ -1,133 +0,0 @@
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

@@ -277,7 +277,31 @@ export async function completeSetup(page: Page): Promise<void> {
if (await setupPage.isVisible()) {
await page.getByTestId('setup-skip-button').click();
}
const moduleSelectionPage = page.getByTestId('ai-module-selection-page');
await expect(moduleSelectionPage).toBeVisible();
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('main-layout')).toBeVisible();
}
export async function openModelsPage(page: Page): Promise<void> {
const settingsPage = page.getByTestId('settings-page');
for (let attempt = 0; attempt < 2 && !(await settingsPage.isVisible()); attempt += 1) {
const settingsLink = page.getByTestId('sidebar-nav-settings');
if (!(await settingsLink.isVisible())) {
await page.getByTestId('sidebar-member-menu-trigger').click();
}
await expect(settingsLink).toBeVisible();
await settingsLink.click();
await settingsPage.waitFor({ state: 'visible', timeout: 5_000 }).catch(() => undefined);
}
await expect(settingsPage).toBeVisible();
await page.getByTestId('settings-reveal-additional').click();
await page
.getByTestId('settings-opencode-models')
.getByRole('button', { name: 'Configure models' })
.click();
await expect(page.getByTestId('models-page')).toBeVisible();
await expect(page.getByTestId('providers-settings')).toBeVisible();
}
export { expect };

View File

@@ -9,8 +9,8 @@ test.describe('AI Design conversations', () => {
try {
const page = await getStableWindow(app);
await page.getByTestId('sidebar-module-switcher-trigger').click();
await page.getByTestId('sidebar-module-painting').click();
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.getByTestId('ai-module-option-painting').click();
await page.getByTestId('sidebar-create-image-project').click();
await page.getByLabel('项目名称').fill('多会话验收项目');

View File

@@ -66,13 +66,15 @@ test.describe('Russian language localization', () => {
// Skip setup
await page.getByTestId('setup-skip-button').click();
await expect(page.getByRole('button', { name: 'Continue in browser' })).toBeVisible();
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await closeElectronApp(app);
firstAppClosed = true;
relaunchedApp = await launchElectronApp({ skipSetup: true });
const relaunchedPage = await getStableWindow(relaunchedApp);
await expect(relaunchedPage.getByTestId('ai-module-selection-page')).toBeVisible();
await relaunchedPage.getByTestId('ai-module-option-programming').click();
await expect(relaunchedPage.getByTestId('main-layout')).toBeVisible();
// Bypass authentication only for the relaunch so the persisted setting
@@ -96,6 +98,8 @@ test.describe('Russian language localization', () => {
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('main-layout')).toBeVisible();
// Navigate to Settings (in English by default after skipSetup)

View File

@@ -1,15 +1,17 @@
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 }) => {
test.describe('Makelore module navigation without setup flow', () => {
test('opens the module chooser and switches between enabled modules', 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]);
expect(minimumSize).toEqual([1024, 700]);
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 expect(page.getByText('请先新建项目')).toHaveCount(0);
await expect(page.getByText('一念成光,万物可创。')).toBeVisible();
@@ -20,28 +22,11 @@ test.describe('NianCode main navigation without setup flow', () => {
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('makelore-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();
@@ -50,28 +35,19 @@ test.describe('NianCode main navigation without setup flow', () => {
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.getByText('AI 设计暂不可用').first()).toBeVisible();
await expect(page.getByTestId('sidebar-image-workspace')).toBeVisible();
await expect(page.getByTestId('sidebar-create-image-project')).toBeVisible();
await expect(page.getByTestId('sidebar-image-projects')).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('请先新建项目')).toHaveCount(0);
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

@@ -57,7 +57,6 @@ async function installFirstChatHost(electronApp: ElectronApplication): Promise<v
schemaVersion: 1,
projectType: 'custom',
initialized: true,
superpowersEnabled: false,
defaultModel: 'niancode-user-models/qwen3.7-plus',
agents: [agent],
knowledgeDirectory: 'knowledge',
@@ -223,6 +222,8 @@ test('submits the first prompt without waiting for the known-empty session histo
let page = await getStableWindow(electronApp);
await page.reload();
page = await getStableWindow(electronApp);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.getByTestId('ai-module-option-programming').click();
const agent = page.getByTestId('project-agent-chat-game-development');
await agent.click();
await expect(agent).toHaveAttribute('aria-pressed', 'true');

View File

@@ -22,6 +22,36 @@ test.describe('OpenCode image compression composer', () => {
updatedAt: '2026-07-18T00:00:00.000Z',
lastOpenedAt: '2026-07-18T00:00:00.000Z',
};
const agent = {
id: 'game-development',
avatarId: 'avatar-01',
roleName: 'Game Developer',
name: 'Game Developer',
builtIn: false,
enabled: true,
model: 'niancode-user-models/qwen3.7-plus',
skillIds: [],
responsibility: {
mission: 'Build and verify the project.',
owns: [],
boundaries: [],
collaborators: [],
principles: [],
},
prompt: '',
archivedAt: null,
pinned: false,
};
const projectConfig = {
schemaVersion: 1,
projectType: 'custom',
initialized: true,
defaultModel: agent.model,
agents: [agent],
knowledgeDirectory: 'knowledge',
createdAt: '2026-07-18T00:00:00.000Z',
updatedAt: '2026-07-18T00:00:00.000Z',
};
const respond = (json: unknown, status = 200) => ({
ok: true,
data: {
@@ -44,7 +74,37 @@ test.describe('OpenCode image compression composer', () => {
url: 'http://127.0.0.1:4096',
});
}
if (path.startsWith('/api/opencode/projects')) {
if (path.startsWith('/api/opencode/projects/config?')) {
return respond({
status: 'valid',
config: projectConfig,
knowledgeFiles: [],
});
}
if (path.startsWith('/api/opencode/projects/template?')) {
return respond({ status: 'missing' });
}
if (path.startsWith('/api/opencode/projects/conversations?')) {
return respond({
state: {
schemaVersion: 1,
sessions: [{
sessionId: 'ses_e2e_compression',
agentId: agent.id,
archivedAt: null,
unreadCount: 0,
createdAt: '2026-07-18T00:00:00.000Z',
updatedAt: '2026-07-18T00:00:00.000Z',
}],
updatedAt: '2026-07-18T00:00:00.000Z',
},
});
}
if (
path === '/api/opencode/projects'
|| path.startsWith('/api/opencode/projects?')
|| (path === '/api/opencode/projects/active' && method === 'GET')
) {
return respond({ projects: [activeProject], activeProject });
}
if (path === '/api/opencode/config-summary') {
@@ -60,6 +120,7 @@ test.describe('OpenCode image compression composer', () => {
sessions: [{
id: 'ses_e2e_compression',
title: 'Image compression',
agent: agent.id,
updatedAt: '2026-07-18T00:00:00.000Z',
}],
});
@@ -88,9 +149,14 @@ test.describe('OpenCode image compression composer', () => {
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();
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.getByTestId('ai-module-option-programming').click();
const composer = page.getByTestId('opencode-message-composer');
await expect(composer).toBeVisible();
const projectAgent = page.getByTestId('project-agent-chat-game-development');
await projectAgent.click();
await expect(projectAgent).toHaveAttribute('aria-pressed', 'true');
await expect(composer.getByRole('button').first()).toBeEnabled();
const png = await sharp({
create: {

View File

@@ -91,6 +91,36 @@ async function installSlashCommandHost(
title: 'Slash E2E',
agent: 'game-development',
};
const agent = {
id: 'game-development',
avatarId: 'avatar-01',
roleName: 'Game Development',
name: 'Game Development',
builtIn: false,
enabled: true,
model: 'niancode-user-models/qwen3.7-plus',
skillIds: [],
responsibility: {
mission: 'Implement and verify project features.',
owns: [],
boundaries: [],
collaborators: [],
principles: [],
},
prompt: '',
archivedAt: null,
pinned: false,
};
const config = {
schemaVersion: 1,
projectType: 'custom',
initialized: true,
defaultModel: 'niancode-user-models/qwen3.7-plus',
agents: [agent],
knowledgeDirectory: 'knowledge',
createdAt: '2026-07-18T00:00:00.000Z',
updatedAt: '2026-07-18T00:00:00.000Z',
};
const status = {
state: 'running',
port: 4096,
@@ -149,13 +179,30 @@ async function installSlashCommandHost(
}
if (path.startsWith('/api/opencode/projects/config?')) {
return respond({
status: 'missing',
status: 'valid',
config,
knowledgeFiles: [],
});
}
if (path.startsWith('/api/opencode/projects/template?')) {
return respond({ status: 'missing' });
}
if (path.startsWith('/api/opencode/projects/conversations?')) {
return respond({
state: {
schemaVersion: 1,
sessions: [{
sessionId: session.id,
agentId: agent.id,
archivedAt: null,
unreadCount: 0,
createdAt: '2026-07-18T00:00:00.000Z',
updatedAt: '2026-07-18T00:00:00.000Z',
}],
updatedAt: '2026-07-18T00:00:00.000Z',
},
});
}
if (path === '/api/opencode/config-summary') {
return respond({
model: 'niancode-user-models/qwen3.7-plus',
@@ -166,16 +213,30 @@ async function installSlashCommandHost(
});
}
if (path === '/api/provider-accounts') {
return respond([]);
return respond([{
id: 'niancode-user-models',
vendorId: 'custom',
label: 'Makelore Models',
authMode: 'api_key',
model: 'qwen3.7-plus',
enabled: true,
isDefault: true,
createdAt: '2026-07-18T00:00:00.000Z',
updatedAt: '2026-07-18T00:00:00.000Z',
}]);
}
if (path === '/api/provider-accounts/key-info') {
return respond([]);
return respond([{
accountId: 'niancode-user-models',
hasKey: true,
keyMasked: 'sk-***',
}]);
}
if (path === '/api/provider-vendors') {
return respond([]);
}
if (path === '/api/provider-accounts/default') {
return respond({ accountId: null });
return respond({ accountId: 'niancode-user-models' });
}
if (path === '/api/opencode/sessions') {
return respond({ sessions: [session] });
@@ -270,18 +331,25 @@ test.describe('OpenCode slash commands', () => {
await completeSetup(page);
await installSlashCommandHost(electronApp);
await page.reload();
await page.getByTestId('sidebar-module-switcher-trigger').click();
await page.getByTestId('sidebar-module-programming').click();
await expect(page).toHaveURL(/\/opencode-chat$/);
await page.getByTestId('project-agent-chat-game-development').click();
const composer = page.getByRole('textbox');
await expect(composer).toBeVisible();
await composer.fill('/comp');
await expect(page.getByRole('option', { name: /compact/ })).toBeVisible();
await composer.press('Enter');
await expect(composer).toHaveValue('/compact');
await expect(
page.getByText('再次按 Enter 执行'),
).toBeVisible();
expect(await readCapturedRequests(electronApp)).toEqual([]);
expect((await readCapturedRequests(electronApp)).some((request) => (
request.path.endsWith('/summarize')
|| request.path.endsWith('/command')
|| request.path.endsWith('/messages')
))).toBe(false);
await composer.press('Enter');
await expect(composer).toHaveValue('');
@@ -310,16 +378,20 @@ test.describe('OpenCode slash commands', () => {
);
await setSlashCommandFailure(electronApp, 'review failed');
const commandCountBeforeFailure = (await readCapturedRequests(electronApp))
.filter((request) => request.path.endsWith('/command')).length;
await composer.fill('/Rev');
await expect(page.getByRole('option', { name: /Review/i })).toBeVisible();
await composer.fill('/Review staged changes ');
await composer.press('Enter');
await expect(page.getByText('review failed')).toBeVisible();
await expect.poll(async () => (
await readCapturedRequests(electronApp)
).filter((request) => request.path.endsWith('/command')).length)
.toBe(commandCountBeforeFailure + 1);
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({
@@ -329,4 +401,94 @@ test.describe('OpenCode slash commands', () => {
model: 'niancode-user-models/qwen3.7-plus',
});
});
test('keeps the project and conversation rails at the minimum width without covering the chat canvas', async ({
electronApp,
page,
}) => {
await completeSetup(page);
await installSlashCommandHost(electronApp);
await page.reload();
await page.setViewportSize({ width: 1280, height: 800 });
await page.getByTestId('sidebar-module-switcher-trigger').click();
await page.getByTestId('sidebar-module-programming').click();
await expect(page).toHaveURL(/\/opencode-chat$/);
await expect(page.getByTestId('opencode-message-composer')).toBeVisible();
const createPartnerButton = page.getByRole('button', { name: '创建伙伴' });
await expect(createPartnerButton).toHaveClass(/text-brand/);
await expect(createPartnerButton).toHaveAttribute('aria-expanded', 'false');
await createPartnerButton.click();
const createPartnerDialog = page.getByRole('dialog', { name: '创建项目伙伴' });
await expect(createPartnerDialog).toBeVisible();
await expect(createPartnerDialog).toHaveCSS('z-index', '110');
await createPartnerDialog.getByLabel(/伙伴名称/).fill('E2E层级检查');
await page.getByRole('button', { name: '取消' }).click();
await expect(createPartnerButton).toHaveClass(/text-brand/);
await expect(page.getByRole('heading', { name: '我的项目空间' })).toHaveCount(0);
await page.getByTestId('agent-browser-panel-open').click();
await expect(page.getByTestId('agent-browser-panel')).toBeVisible();
const diagnostics = page.getByTestId('agent-browser-diagnostics');
await expect(diagnostics).toHaveAttribute('data-state', 'closed');
await page.getByRole('tab', { name: /Console/ }).click();
await expect(diagnostics).toHaveAttribute('data-state', 'open');
await expect(diagnostics).toHaveCSS('height', '260px');
await page.getByRole('button', { name: '收起调试面板' }).click();
await expect(diagnostics).toHaveAttribute('data-state', 'closed');
const rect = async (testId: string) => page.getByTestId(testId).evaluate((element) => {
const box = element.getBoundingClientRect();
return { top: box.top, left: box.left, right: box.right, width: box.width };
});
const [layout, project, conversations, conversationHeader, conversationContext, chat, browser] = await Promise.all([
rect('opencode-chat-layout'),
rect('sidebar'),
rect('agent-conversation-sidebar'),
rect('agent-conversation-sidebar-header'),
rect('agent-conversation-dialog-context'),
rect('opencode-chat-canvas'),
rect('agent-browser-panel'),
]);
const isHeaderInsideConversationSidebar = async () => page.getByTestId('agent-conversation-sidebar').evaluate((sidebar) => sidebar.contains(document.querySelector('[data-testid="agent-conversation-sidebar-header"]')));
expect(project.width).toBeGreaterThanOrEqual(255);
expect(project.width).toBeLessThan(257);
expect(conversations.width).toBeGreaterThanOrEqual(255);
expect(conversations.width).toBeLessThan(257);
expect(Math.abs(conversations.top - project.top)).toBeLessThan(2);
expect(Math.abs(conversationHeader.top)).toBeLessThan(2);
expect(Math.abs(conversationHeader.left - conversations.left)).toBeLessThan(2);
expect(Math.abs(conversationHeader.width - conversations.width)).toBeLessThan(2);
expect(Math.abs(conversationContext.top)).toBeLessThan(2);
expect(Math.abs(conversationContext.left - chat.left)).toBeLessThan(2);
expect(await isHeaderInsideConversationSidebar()).toBe(false);
expect(Math.abs(conversations.left - project.right)).toBeLessThan(2);
expect(Math.abs(chat.left - conversations.right)).toBeLessThan(2);
expect(Math.abs(browser.right - layout.right)).toBeLessThan(2);
expect(browser.left).toBeGreaterThanOrEqual(chat.right - 1);
const readRailAlignment = async () => {
const [collapsedProject, collapsedConversations] = await Promise.all([
rect('sidebar'),
rect('agent-conversation-sidebar'),
]);
return {
projectWidth: collapsedProject.width,
conversationLeft: collapsedConversations.left,
projectRight: collapsedProject.right,
alignmentDelta: Math.abs(collapsedConversations.left - collapsedProject.right),
};
};
await page.getByRole('button', { name: '折叠侧栏' }).click();
await expect.poll(async () => (await readRailAlignment()).projectWidth).toBeLessThan(1);
await expect.poll(async () => (await readRailAlignment()).conversationLeft).toBeLessThan(2);
await expect.poll(isHeaderInsideConversationSidebar).toBe(false);
await page.getByRole('button', { name: '展开侧栏' }).click();
await expect.poll(async () => (await readRailAlignment()).alignmentDelta).toBeLessThan(2);
await expect.poll(async () => (await readRailAlignment()).projectWidth).toBeGreaterThan(255);
await expect.poll(isHeaderInsideConversationSidebar).toBe(false);
});
});

View File

@@ -0,0 +1,115 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
import { createProjectConfig } from '../../shared/project-config';
test.describe('Project configuration skills', () => {
test('does not expose the removed Superpowers feature and supports Skill details', async ({ launchElectronApp }) => {
const parentPath = await mkdtemp(path.join(tmpdir(), 'niancode-project-configuration-e2e-'));
const app = await launchElectronApp({ skipSetup: true });
try {
const userDataDir = await app.evaluate(({ app: electronApp }) => electronApp.getPath('userData'));
const skillDir = path.join(userDataDir, 'opencode', 'niancode-config', 'skills', 'pm-project-plan');
await mkdir(path.join(skillDir, 'references'), { recursive: true });
await writeFile(path.join(skillDir, 'SKILL.md'), '---\nname: pm-project-plan\ndescription: E2E project planning skill.\n---\n\n# Project planning\n');
await writeFile(path.join(skillDir, 'references', 'guide.md'), '# Reference guide\n');
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 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.getByTestId('sidebar-create-project').click();
await expect(page.getByRole('radio', { name: '小游戏' })).toBeChecked();
await expect(page.getByRole('radio', { name: '小程序' })).not.toBeChecked();
await expect(page.getByRole('radio', { name: '自定义项目' })).not.toBeChecked();
await page.getByRole('radio', { name: '小程序' }).click();
await expect(page.getByRole('radio', { name: '小程序' })).toBeChecked();
await page.getByRole('radio', { name: '小游戏' }).click();
await expect(page.getByRole('radio', { name: '直接使用所选文件夹(默认)' })).toBeChecked();
await page.getByRole('button', { name: '选择路径' }).click();
await expect(page.getByLabel('项目路径')).toHaveValue(parentPath);
await page.getByRole('button', { name: '确认创建' }).click();
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
await expect(page.getByTestId('sidebar-nav-publish')).toHaveCount(0);
await expect(page.getByText(/新项目还没有伙伴/)).toHaveCount(0);
await expect(page.getByTestId('project-configuration-actions')).toBeVisible();
const actionBarBottomGap = await page.getByTestId('project-configuration-actions').evaluate((element) => window.innerHeight - element.getBoundingClientRect().bottom);
expect(actionBarBottomGap).toBeLessThan(40);
await expect(page.getByRole('button', { name: '创建伙伴' })).toBeVisible();
for (const [resourceId, title] of [
['resource-card-models', '大脑(大语言模型)'],
['resource-card-knowledge', '笔记本(知识库)'],
['resource-card-skills', '工具箱(技能)'],
] as const) {
await page.getByTestId(resourceId).click();
const drawer = page.getByRole('dialog', { name: title });
await expect(drawer).toBeVisible();
if (resourceId === 'resource-card-models') {
await expect(drawer.getByRole('combobox')).toHaveCount(0);
await expect(drawer.locator('[data-testid="project-model-list"], [data-testid="project-model-empty-state"]')).toHaveCount(1);
}
await page.getByRole('button', { name: '关闭' }).click();
await expect(page.getByRole('dialog', { name: title })).toHaveCount(0);
}
const now = new Date().toISOString();
await mkdir(path.join(parentPath, '.niancode'), { recursive: true });
await writeFile(path.join(parentPath, '.niancode', 'project.json'), JSON.stringify({
...createProjectConfig(undefined, 'mini_game'),
initialized: true,
agents: [{
id: 'e2e-partner',
avatarId: 'avatar-01',
roleName: '项目伙伴',
name: 'E2E伙伴',
builtIn: false,
enabled: true,
model: 'moonshot/kimi-k2.6',
skillIds: [],
responsibility: { mission: '验证伙伴维护弹窗。', owns: [], boundaries: [], collaborators: [], principles: [] },
prompt: '',
archivedAt: null,
pinned: false,
createdAt: now,
updatedAt: now,
}],
}, null, 2));
await page.reload();
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
await page.getByTestId('resource-card-skills').click();
await expect(page.getByTestId('superpowers-card')).toHaveCount(0);
await expect(page.getByRole('switch', { name: '启用 Superpowers' })).toHaveCount(0);
await expect(page.getByTestId('skill-card-pm-project-plan')).toBeVisible();
await page.getByTestId('skill-card-pm-project-plan').click();
await expect(page.getByTestId('skill-detail-view')).toBeVisible();
await expect(page.getByTestId('skill-structure')).toContainText('references/guide.md');
await expect(page.getByTestId('skill-main-file')).toContainText('# Project planning');
await page.getByRole('button', { name: '返回技能列表' }).click();
await expect(page.getByTestId('skill-card-pm-project-plan')).toBeVisible();
await page.getByRole('button', { name: '关闭' }).click();
await expect(page.getByTestId('resource-card-publish')).toHaveCount(0);
await expect(page.getByRole('button', { name: '一键提交审核' })).toBeVisible();
const agentCard = page.getByTestId('project-agent-e2e-partner');
await expect(agentCard).toContainText('E2E伙伴');
await agentCard.click();
const maintenanceDialog = page.getByRole('dialog', { name: 'E2E伙伴 · 伙伴维护' });
await expect(maintenanceDialog).toBeVisible();
await expect(maintenanceDialog.getByLabel(/伙伴名称/)).toHaveValue('E2E伙伴');
await expect(page.getByText('完成配置')).toHaveCount(0);
} finally {
await closeElectronApp(app);
await rm(parentPath, { recursive: true, force: true });
}
});
});

View File

@@ -1,47 +0,0 @@
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 publishable projects with Superpowers disabled and only the one-click release entry', 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 expect(page.getByRole('radio', { name: '小程序' })).not.toBeChecked();
await expect(page.getByRole('radio', { name: '自定义项目' })).not.toBeChecked();
await page.getByRole('radio', { name: '小程序' }).click();
await expect(page.getByRole('radio', { name: '小程序' })).toBeChecked();
await page.getByRole('radio', { name: '小游戏' }).click();
await expect(page.getByRole('radio', { name: '直接使用所选文件夹(默认)' })).toBeChecked();
await page.getByRole('button', { name: '选择路径' }).click();
await expect(page.getByLabel('项目路径')).toHaveValue(parentPath);
await page.getByRole('button', { name: '确认创建' }).click();
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
await expect(page.getByTestId('sidebar-nav-publish')).toHaveCount(0);
await expect(page.getByText(/新项目还没有联系人/)).toBeVisible();
await expect(page.getByRole('button', { name: '新增伙伴' })).toBeVisible();
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);
await expect(page.getByRole('button', { name: '一键提交审核' })).toHaveCount(1);
await expect(page.getByText('自动部署', { exact: false })).toHaveCount(0);
await expect(page.getByText('部署发布检查', { exact: false })).toHaveCount(0);
} finally {
await closeElectronApp(app);
await rm(parentPath, { recursive: true, force: true });
}
});
});

View File

@@ -1,4 +1,4 @@
import { completeSetup, expect, test } from './fixtures/electron';
import { completeSetup, expect, openModelsPage, test } from './fixtures/electron';
const TEST_PROVIDER_ID = 'moonshot-e2e';
const TEST_PROVIDER_LABEL = 'Moonshot E2E';
@@ -6,16 +6,29 @@ 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,
const result = await window.electron.ipcRenderer.invoke('hostapi:fetch', {
path: '/api/provider-accounts',
method: 'POST',
body: {
account: {
id: providerId,
vendorId: 'moonshot',
label: providerLabel,
authMode: 'api_key',
baseUrl: 'https://api.moonshot.cn/v1',
model: 'kimi-k2.6',
enabled: true,
isDefault: false,
createdAt: now,
updatedAt: now,
},
apiKey: 'sk-e2e-provider-key',
},
});
if (!result?.ok || !result.data?.ok || result.data.json?.success !== true) {
throw new Error(`Failed to seed provider: ${JSON.stringify(result)}`);
}
}, { providerId: TEST_PROVIDER_ID, providerLabel: TEST_PROVIDER_LABEL });
}
@@ -24,8 +37,7 @@ test.describe('NianCode provider lifecycle', () => {
await completeSetup(page);
await seedTestProvider(page);
await page.getByTestId('sidebar-nav-models').click();
await expect(page.getByTestId('providers-settings')).toBeVisible();
await openModelsPage(page);
await expect(page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toContainText(TEST_PROVIDER_LABEL);
await page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`).hover();
@@ -39,7 +51,7 @@ test.describe('NianCode provider lifecycle', () => {
await completeSetup(page);
await seedTestProvider(page);
await page.getByTestId('sidebar-nav-models').click();
await openModelsPage(page);
await expect(page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toContainText(TEST_PROVIDER_LABEL);
await page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`).hover();
@@ -52,10 +64,11 @@ test.describe('NianCode provider lifecycle', () => {
try {
const relaunchedPage = await relaunchedApp.firstWindow();
await relaunchedPage.waitForLoadState('domcontentloaded');
await expect(relaunchedPage.getByTestId('ai-module-selection-page')).toBeVisible();
await relaunchedPage.getByTestId('ai-module-option-programming').click();
await expect(relaunchedPage.getByTestId('main-layout')).toBeVisible();
await relaunchedPage.getByTestId('sidebar-nav-models').click();
await expect(relaunchedPage.getByTestId('providers-settings')).toBeVisible();
await openModelsPage(relaunchedPage);
await expect(relaunchedPage.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toHaveCount(0);
await expect(relaunchedPage.getByText(TEST_PROVIDER_LABEL)).toHaveCount(0);
} finally {
@@ -147,8 +160,7 @@ test.describe('NianCode provider lifecycle', () => {
});
});
await page.getByTestId('sidebar-nav-models').click();
await expect(page.getByTestId('providers-settings')).toBeVisible();
await openModelsPage(page);
await page.getByTestId('providers-add-button').click();
await expect(page.getByTestId('add-provider-dialog')).toBeVisible();
@@ -160,7 +172,7 @@ test.describe('NianCode provider lifecycle', () => {
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');
await expect(page.getByTestId(/^provider-card-custom-/).filter({ hasText: 'LM Studio Local' })).toBeVisible();
});
test('edit form validates the new API key inline before saving (single button)', async ({ electronApp, page }) => {
@@ -223,8 +235,7 @@ test.describe('NianCode provider lifecycle', () => {
});
});
await page.getByTestId('sidebar-nav-models').click();
await expect(page.getByTestId('providers-settings')).toBeVisible();
await openModelsPage(page);
await expect(page.getByTestId('provider-card-moonshot-edit')).toBeVisible();
await page.getByTestId('provider-card-moonshot-edit').hover();

View File

@@ -1,4 +1,4 @@
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
import { closeElectronApp, expect, getStableWindow, openModelsPage, test } from './fixtures/electron';
test.describe('hover-only scrollbar visibility', () => {
test('hides scrollbars until a scroll container is hovered', async ({ launchElectronApp }) => {
@@ -6,8 +6,9 @@ test.describe('hover-only scrollbar visibility', () => {
try {
const page = await getStableWindow(app);
await page.getByTestId('sidebar-nav-models').click();
await expect(page.getByTestId('models-page')).toBeVisible();
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.getByTestId('ai-module-option-programming').click();
await openModelsPage(page);
const scrollContainer = page.locator('[data-testid="models-page"] .overflow-y-auto').first();
await expect(scrollContainer).toBeVisible();

View File

@@ -27,6 +27,7 @@ test.describe('NianCode developer proxy settings', () => {
await openSettingsFromSidebarAccountMenu(page);
await expect(page.getByTestId('settings-page')).toBeVisible();
await page.getByTestId('settings-reveal-additional').click();
const devModeToggle = page.getByTestId('settings-dev-mode-switch');
await expect(devModeToggle).toBeVisible();

View File

@@ -28,6 +28,8 @@ test.describe('NianCode window zoom shortcuts', () => {
try {
const page = await getStableWindow(app);
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 app.evaluate(({ BrowserWindow }) => {