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

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

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);
});
});