Merge branch 'main' of https://git.nianxx.cn/wangxuming/makelore
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
2026-08-15 10:40:33 +08:00
31 changed files with 1153 additions and 241 deletions

View File

@@ -344,6 +344,40 @@ describe('AgentConversationSidebar', () => {
})));
});
it('defaults only the planning Skills and leaves presentation optional', async () => {
hostApiFetchMock.mockResolvedValue({
skills: [
{ name: 'agent-browser' },
{ name: 'frontend-slides' },
{ name: 'grilling' },
{ name: 'planning-with-files' },
],
});
const callbacks = renderSidebar();
fireEvent.click(screen.getByRole('button', { name: '创建伙伴' }));
const grilling = await screen.findByLabelText('绑定技能:方案质询');
const planning = await screen.findByLabelText('绑定技能:项目规划');
const slides = await screen.findByLabelText('绑定技能:项目演示');
const browser = await screen.findByLabelText('绑定技能:开发浏览器');
await waitFor(() => {
expect(grilling).toBeChecked();
expect(planning).toBeChecked();
expect(slides).not.toBeChecked();
expect(browser).not.toBeChecked();
});
fireEvent.click(grilling);
expect(grilling).not.toBeChecked();
fireEvent.change(screen.getByLabelText(/伙伴名称/), { target: { value: '小李' } });
fireEvent.change(screen.getByLabelText(/职责简述/), { target: { value: '负责整理资料。' } });
fireEvent.click(screen.getByRole('button', { name: '创建并开始对话' }));
await waitFor(() => expect(callbacks.onCreateAgent).toHaveBeenCalledWith(expect.objectContaining({
skillIds: ['planning-with-files'],
})));
});
it('shows advanced creation settings directly below the basics', () => {
renderSidebar();
fireEvent.click(screen.getByRole('button', { name: '创建伙伴' }));

View File

@@ -27,7 +27,7 @@ describe('AI module navigation', () => {
useOpencodeStore.setState({ activeProject: null });
});
it('shows the four module choices and routes enabled modules from the chooser', async () => {
it('shows the four module choices and routes Canvas from the chooser', async () => {
render(
<MemoryRouter initialEntries={['/module-select']}>
<Routes>
@@ -43,6 +43,9 @@ describe('AI module navigation', () => {
expect(screen.getByTestId('ai-module-options')).toHaveClass('module-selection-options');
expect(screen.getByText('zhangsan欢迎回来')).toBeInTheDocument();
expect(screen.getByTestId('ai-module-option-programming')).toHaveClass('module-option-card', 'module-option-card-horizontal');
for (const moduleId of ['programming', 'painting', 'learning', 'robot'] as const) {
expect(screen.getByTestId(`ai-module-option-${moduleId}`).querySelector('.module-option-artwork')).toBeNull();
}
expect(screen.queryByText('先选一个入口。进入后,你还可以从左下角随时切换到其他模块。')).not.toBeInTheDocument();
expect(screen.queryByText('选择创作空间。你可以随时从侧边栏切换。')).not.toBeInTheDocument();
expect(screen.getByAltText('Makelore')).toHaveClass('h-8', 'w-40', 'object-cover');
@@ -55,20 +58,35 @@ describe('AI module navigation', () => {
expect(screen.getByAltText('数位板设计创作')).toBeInTheDocument();
expect(screen.getByTestId('ai-module-option-painting')).toHaveTextContent('用灵感描绘色彩缤纷的世界');
const learningOption = screen.getByTestId('ai-module-option-learning');
expect(learningOption).toBeDisabled();
expect(learningOption).toHaveClass('module-option-card-disabled');
expect(learningOption).toBeEnabled();
expect(learningOption).not.toHaveClass('module-option-card-disabled');
expect(learningOption).toHaveTextContent('Learning 学习');
expect(learningOption).toHaveTextContent('用顶尖的方法解锁万物规律');
expect(learningOption).toHaveTextContent('暂未开放');
expect(screen.getByAltText('数学科技宇宙')).toBeInTheDocument();
const robotOption = screen.getByTestId('ai-module-option-robot');
expect(robotOption).toBeEnabled();
expect(robotOption).not.toHaveClass('module-option-card-disabled');
expect(robotOption).toHaveTextContent('Robot 机器');
expect(robotOption).toHaveTextContent('制作你的第一个机器人小伙伴');
expect(robotOption).not.toHaveTextContent('暂未开放');
expect(screen.getByAltText('青少年管理机器人硬件与设备绑定')).toBeInTheDocument();
fireEvent.click(robotOption);
fireEvent.click(learningOption);
expect(screen.getByTestId('ai-module-selection-page')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('ai-module-option-painting'));
expect(await screen.findByTestId('location-path')).toHaveTextContent('/image-canvas');
});
it('routes Robot from the chooser to the AI hardware workspace', async () => {
render(
<MemoryRouter initialEntries={['/module-select']}>
<Routes>
<Route path="/module-select" element={<ModuleSelection />} />
<Route path="*" element={<LocationProbe />} />
</Routes>
</MemoryRouter>,
);
fireEvent.click(screen.getByTestId('ai-module-option-robot'));
expect(await screen.findByTestId('location-path')).toHaveTextContent('/ai-hardware');
});
@@ -112,7 +130,7 @@ describe('AI module navigation', () => {
expect(await screen.findByTestId('location-path')).toHaveTextContent('/login');
});
it('keeps the current module active and switches from the Sidebar entry', async () => {
it('returns to the module chooser from the Sidebar entry', async () => {
render(
<MemoryRouter initialEntries={['/image-canvas']}>
<Routes>
@@ -121,29 +139,16 @@ describe('AI module navigation', () => {
</MemoryRouter>,
);
expect(screen.getByTestId('sidebar-module-switcher-trigger')).toHaveAttribute('aria-expanded', 'false');
expect(screen.getByTestId('sidebar-module-switcher-trigger')).toHaveTextContent('绘画 Canvas');
expect(screen.getByTestId('sidebar-module-switcher-menu')).toHaveAttribute('data-state', 'closed');
expect(screen.getByTestId('sidebar-module-painting')).toBeInTheDocument();
expect(screen.getByTestId('sidebar-module-switcher-trigger')).toHaveAttribute('aria-label', '返回首页');
expect(screen.getByTestId('sidebar-module-switcher-trigger')).toHaveClass('p-3');
expect(screen.queryByTestId('sidebar-module-switcher-menu')).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId('sidebar-module-switcher-trigger'));
expect(screen.getByTestId('sidebar-module-switcher-menu')).toBeInTheDocument();
expect(screen.getByTestId('sidebar-module-painting')).toHaveAttribute('aria-current', 'page');
expect(screen.getByTestId('sidebar-module-programming')).not.toHaveAttribute('aria-current');
expect(screen.getByTestId('sidebar-module-robot')).not.toHaveAttribute('aria-current');
const learningModule = screen.getByTestId('sidebar-module-learning');
expect(learningModule).toBeDisabled();
expect(learningModule).toHaveTextContent('学习 Learning');
expect(learningModule).toHaveTextContent('AI 学习');
expect(learningModule).not.toHaveAttribute('aria-current');
fireEvent.click(learningModule);
expect(screen.getByTestId('location-path')).toHaveTextContent('/image-canvas');
fireEvent.click(screen.getByTestId('sidebar-module-programming'));
expect(await screen.findByTestId('location-path')).toHaveTextContent('/opencode-chat');
expect(await screen.findByTestId('location-path')).toHaveTextContent('/module-select');
});
it('marks Robot active on the AI hardware route and switches back to Canvas', async () => {
it('shows Robot as the active module and returns to the chooser from the AI hardware route', async () => {
render(
<MemoryRouter initialEntries={['/ai-hardware']}>
<Routes>
@@ -154,11 +159,7 @@ describe('AI module navigation', () => {
expect(screen.getByTestId('sidebar-module-switcher-trigger')).toHaveTextContent('Robot 机器');
fireEvent.click(screen.getByTestId('sidebar-module-switcher-trigger'));
expect(screen.getByTestId('sidebar-module-robot')).toHaveAttribute('aria-current', 'page');
expect(screen.getByTestId('sidebar-module-programming')).not.toHaveAttribute('aria-current');
fireEvent.click(screen.getByTestId('sidebar-module-painting'));
expect(await screen.findByTestId('location-path')).toHaveTextContent('/image-canvas');
expect(await screen.findByTestId('location-path')).toHaveTextContent('/module-select');
});
it('keeps breathing room around the compact module switcher trigger', () => {
@@ -169,7 +170,7 @@ describe('AI module navigation', () => {
);
const trigger = screen.getByTestId('sidebar-module-switcher-trigger');
expect(trigger).toHaveClass('inline-flex', 'w-fit', 'px-2');
expect(trigger).toHaveClass('inline-flex', 'w-fit', 'p-2');
});
});

View File

@@ -183,6 +183,22 @@ describe('project-owned contact configuration', () => {
expect(markdown).toContain('"*": deny');
});
it('grants selected presentation and planning Skills to the materialized OpenCode Agent', async () => {
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-config-planning-skills-'));
const initial = await createInitialProjectConfig(projectPath);
await writeProjectConfig(projectPath, {
...initial,
initialized: true,
agents: [createContact({ skillIds: ['frontend-slides', 'grilling', 'planning-with-files'] })],
});
const markdown = await readFile(path.join(projectPath, '.opencode', 'agent', 'agent-test.md'), 'utf8');
expect(markdown).toContain(' "*": deny\n frontend-slides: allow\n grilling: allow\n planning-with-files: allow');
expect(markdown).toContain('主动调用 `frontend-slides`');
expect(markdown).toContain('主动调用 `grilling`');
expect(markdown).toContain('主动调用 `planning-with-files`');
});
it('denies unselected skills when a contact has selected skills', async () => {
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-config-selected-skills-'));
const initial = await createInitialProjectConfig(projectPath);

View File

@@ -150,8 +150,11 @@ describe('ProjectConfiguration', () => {
] as const;
for (const [resourceId, title] of resourceDrawers) {
fireEvent.click(screen.getByTestId(resourceId));
expect(await screen.findByRole('dialog', { name: title })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '关闭' }));
const drawer = await screen.findByRole('dialog', { name: title });
expect(drawer).toBeInTheDocument();
const closeButton = within(drawer).getByRole('button', { name: '关闭' });
expect(closeButton).toHaveClass('h-10', 'w-10', 'shrink-0');
fireEvent.click(closeButton);
await waitFor(() => expect(screen.queryByRole('dialog', { name: title })).not.toBeInTheDocument());
}
});

View File

@@ -321,24 +321,14 @@ describe('Sidebar project initialization flow', () => {
expect(screen.queryByText('课程路径')).not.toBeInTheDocument();
expect(screen.queryByText('伙伴广场')).not.toBeInTheDocument();
const moduleTrigger = screen.getByTestId('sidebar-module-switcher-trigger');
expect(moduleTrigger).toHaveAttribute('aria-expanded', 'false');
expect(moduleTrigger).toHaveAttribute('aria-label', '返回首页');
expect(moduleTrigger).toHaveClass('bg-transparent');
expect(moduleTrigger).toHaveClass('w-fit', 'justify-start', 'gap-1');
expect(moduleTrigger).toHaveClass('w-fit', 'justify-start', 'p-2');
expect(moduleTrigger).not.toHaveClass('bg-brand-soft');
expect(moduleTrigger).toHaveTextContent('编程 Code');
expect(screen.getByTestId('sidebar-module-switcher-menu')).toHaveAttribute('data-state', 'closed');
expect(screen.getByTestId('sidebar-module-programming')).toBeInTheDocument();
expect(screen.queryByTestId('sidebar-module-switcher-menu')).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId('sidebar-module-switcher-trigger'));
const programmingModule = screen.getByTestId('sidebar-module-programming');
expect(programmingModule).toHaveTextContent('编程 Code');
expect(programmingModule).toHaveClass('bg-brand-soft');
expect(programmingModule.querySelector('svg')).not.toBeInTheDocument();
expect(screen.getByTestId('sidebar-module-painting')).toHaveTextContent('绘画 Canvas');
expect(screen.getByTestId('sidebar-module-painting')).not.toHaveClass('bg-brand-soft');
expect(screen.getByTestId('sidebar-module-programming')).not.toHaveTextContent('AI');
expect(screen.getByTestId('sidebar-module-painting')).not.toHaveTextContent('AI');
expect(screen.getByTestId('sidebar-module-programming')).not.toHaveTextContent('Makelore');
expect(screen.getByTestId('sidebar-module-painting')).not.toHaveTextContent('Makelore');
expect(navigateMock).toHaveBeenCalledWith('/module-select');
expect(screen.queryByTestId('sidebar-nav-image-canvas')).not.toBeInTheDocument();
expect(await screen.findByText(/暂无真实项目/)).toBeInTheDocument();
});

View File

@@ -7,6 +7,18 @@ describe('skill display metadata', () => {
name: '开发浏览器',
description: '打开、查看或调试本地及公网网页,读取 Console、Network、DOM 和样式信息。',
});
expect(getSkillDisplayInfo('frontend-slides')).toEqual({
name: '项目演示',
description: '把项目内容整理成可播放的 16:9 HTML 幻灯片,不生成 PPTX 或云端发布。',
});
expect(getSkillDisplayInfo('grilling')).toEqual({
name: '方案质询',
description: '在重要工作开始前逐项澄清目标、范围和关键取舍,确认后再执行。',
});
expect(getSkillDisplayInfo('planning-with-files')).toEqual({
name: '项目规划',
description: '为复杂任务维护可恢复的计划、发现和进展,记录在项目的 .niancode/agent-planning/ 中。',
});
});
it('does not expose an unknown external runtime id', () => {

View File

@@ -2,7 +2,12 @@ import { describe, expect, it } from 'vitest';
import { BUNDLED_COURSE_SKILL_IDS } from '@electron/opencode/course-skills';
describe('bundled Skill allowlist', () => {
it('keeps only the explicitly retained Agent Browser Skill', () => {
expect([...BUNDLED_COURSE_SKILL_IDS]).toEqual(['agent-browser']);
it('keeps only the explicitly retained project Skills', () => {
expect([...BUNDLED_COURSE_SKILL_IDS]).toEqual([
'agent-browser',
'frontend-slides',
'grilling',
'planning-with-files',
]);
});
});