feat(agents): 新增云端智能体入口与草稿编辑
This commit is contained in:
65
tests/e2e/cloud-agents.spec.ts
Normal file
65
tests/e2e/cloud-agents.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
test('personal cloud Agent creation, draft save and leave protection in Electron', async ({ launchElectronApp }, testInfo) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
const applicationUrl = page.url();
|
||||
await page.goto('about:blank');
|
||||
// This UI test owns the Host API boundary; real cloud HTTP/PG is tested separately.
|
||||
await app.evaluate(({ ipcMain }) => {
|
||||
let draft: Record<string, unknown> | null = null;
|
||||
const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } });
|
||||
ipcMain.removeHandler('hostapi:fetch');
|
||||
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string; method?: string; body?: string }) => {
|
||||
const path = request.path ?? '';
|
||||
const method = request.method ?? 'GET';
|
||||
if (path === '/api/auth/session/sync') return result({
|
||||
success: true, session: { accessToken: 'ui-fixture', tokenType: 'Bearer', expiresAt: Date.now() + 3600000, lastActiveAt: Date.now(), canRefresh: false },
|
||||
});
|
||||
if (path === '/api/auth/me') return result({
|
||||
success: true, user: { username: 'creator', userId: 'creator', tenantId: null, deptId: null, authorities: [] },
|
||||
moduleAccess: { programming: true, design: true, robot: true, cloud_agents: true },
|
||||
});
|
||||
if (path === '/api/works/user/agent-profile') return result({
|
||||
success: true, profile: { display_name: '创作者', age: null, gender: null, avatar_url: null, share_age_with_agents: false, share_gender_with_agents: false, analysis_enabled: true, completed: true, version: 1, updated_at: '2026-09-10T00:00:00Z' },
|
||||
});
|
||||
if (path.startsWith('/api/cloud-agents/')) {
|
||||
const body = typeof request.body === 'string' ? JSON.parse(request.body) : request.body ?? {};
|
||||
if (method === 'POST') draft = {
|
||||
slug: 'ml-' + 'a'.repeat(32), name: body.name, purpose: body.purpose, system_prompt: body.purpose,
|
||||
draft_revision: 1, updated_at: '2026-09-10T00:00:00Z',
|
||||
};
|
||||
if (method === 'PATCH') draft = { ...draft, name: body.name, purpose: body.purpose, system_prompt: body.system_prompt, draft_revision: 2 };
|
||||
if (method === 'GET' && (path.endsWith('/agents') || path.endsWith('/bootstrap'))) return result({ agents: draft ? [draft] : [], next_cursor: null });
|
||||
return result(draft);
|
||||
}
|
||||
return result({ success: true });
|
||||
});
|
||||
});
|
||||
await page.goto(applicationUrl);
|
||||
await page.getByTestId('ai-module-option-cloud_agents').click();
|
||||
await expect(page.getByTestId('sidebar-cloud-agents-navigation')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-robot-navigation')).toHaveCount(0);
|
||||
await page.getByRole('button', { name: '创建第一个智能体' }).click();
|
||||
await page.getByLabel('名称', { exact: true }).fill('我的写作搭档');
|
||||
await page.getByLabel('用途', { exact: true }).fill('整理灵感,把想法写成清晰的文章');
|
||||
await page.getByRole('button', { name: '创建草稿', exact: true }).click();
|
||||
await expect(page.getByTestId('cloud-agent-editor')).toBeVisible();
|
||||
await page.getByLabel('角色与指令').fill('使用简洁自然的中文。先理解我的目的,再协助梳理结构。');
|
||||
await page.getByTestId('sidebar-module-switcher-trigger').click();
|
||||
await expect(page.getByRole('dialog', { name: '未保存的修改' })).toBeVisible();
|
||||
await page.getByRole('button', { name: '继续编辑', exact: true }).click();
|
||||
await page.getByRole('button', { name: '保存草稿', exact: true }).click();
|
||||
await expect(page.getByRole('button', { name: '已保存', exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: testInfo.outputPath('cloud-agent-editor.png') });
|
||||
await page.getByTestId('sidebar-module-switcher-trigger').click();
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
await page.getByTestId('ai-module-option-cloud_agents').click();
|
||||
await page.getByRole('button', { name: /我的写作搭档/ }).click();
|
||||
await expect(page.getByLabel('角色与指令')).toHaveValue('使用简洁自然的中文。先理解我的目的,再协助梳理结构。');
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
@@ -81,7 +81,7 @@ test.describe('Makelore module navigation without setup flow', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps the three module entries in a proportional vertical stack while resizing the window', async ({ launchElectronApp }) => {
|
||||
test('keeps the four module entries in a proportional vertical stack while resizing the window', async ({ launchElectronApp }) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
try {
|
||||
@@ -103,7 +103,7 @@ test.describe('Makelore module navigation without setup flow', () => {
|
||||
}));
|
||||
|
||||
const initialMetrics = await readCardMetrics();
|
||||
expect(initialMetrics).toHaveLength(3);
|
||||
expect(initialMetrics).toHaveLength(4);
|
||||
await expect(moduleCards.first().locator('.module-option-card-arrow')).toHaveCount(0);
|
||||
await expect(moduleCards.first().locator('.module-option-artwork')).toHaveCount(0);
|
||||
await expect.poll(async () => await moduleCards.first().evaluate((element) => {
|
||||
@@ -155,7 +155,7 @@ test.describe('Makelore module navigation without setup flow', () => {
|
||||
await expect.poll(async () => (await readCardMetrics())[0]?.width ?? 0).toBeLessThan(initialWidth - 1);
|
||||
|
||||
const resizedMetrics = await readCardMetrics();
|
||||
expect(resizedMetrics).toHaveLength(3);
|
||||
expect(resizedMetrics).toHaveLength(4);
|
||||
expect(Math.max(...resizedMetrics.map((card) => card.left)) - Math.min(...resizedMetrics.map((card) => card.left))).toBeLessThan(1);
|
||||
expect(resizedMetrics.map((card) => card.top)).toEqual(
|
||||
[...resizedMetrics].sort((top, bottom) => top.top - bottom.top).map((card) => card.top),
|
||||
@@ -195,7 +195,7 @@ test.describe('Makelore module navigation without setup flow', () => {
|
||||
const page = await getStableWindow(app);
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
|
||||
await expect(page.locator('[data-testid^="ai-module-option-"]')).toHaveCount(3);
|
||||
await expect(page.locator('[data-testid^="ai-module-option-"]')).toHaveCount(4);
|
||||
await expect(page.getByTestId('ai-module-option-learning')).toHaveCount(0);
|
||||
await page.evaluate(() => {
|
||||
window.location.hash = '#/learning';
|
||||
|
||||
@@ -88,6 +88,7 @@ describe('auth host api routes', () => {
|
||||
design: true,
|
||||
learning: false,
|
||||
robot: true,
|
||||
cloud_agents: true,
|
||||
},
|
||||
}), { status: 200 }),
|
||||
);
|
||||
@@ -116,6 +117,7 @@ describe('auth host api routes', () => {
|
||||
programming: false,
|
||||
design: true,
|
||||
robot: true,
|
||||
cloud_agents: true,
|
||||
},
|
||||
});
|
||||
expect(response.json()).not.toHaveProperty('moduleAccess.learning');
|
||||
@@ -162,6 +164,7 @@ describe('auth host api routes', () => {
|
||||
programming: true,
|
||||
design: true,
|
||||
robot: true,
|
||||
cloud_agents: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ function resetAuthStore() {
|
||||
programming: true,
|
||||
design: true,
|
||||
robot: true,
|
||||
cloud_agents: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -88,6 +89,7 @@ describe('auth store', () => {
|
||||
programming: true,
|
||||
design: false,
|
||||
robot: false,
|
||||
cloud_agents: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -128,6 +130,7 @@ describe('auth store', () => {
|
||||
programming: true,
|
||||
design: false,
|
||||
robot: false,
|
||||
cloud_agents: true,
|
||||
});
|
||||
expect(window.localStorage.getItem('niancode-auth')).not.toContain(
|
||||
'must-not-return-to-renderer-storage',
|
||||
@@ -177,6 +180,7 @@ describe('auth store', () => {
|
||||
programming: true,
|
||||
design: true,
|
||||
robot: false,
|
||||
cloud_agents: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -214,6 +218,7 @@ describe('auth store', () => {
|
||||
programming: true,
|
||||
design: true,
|
||||
robot: true,
|
||||
cloud_agents: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -257,6 +262,7 @@ describe('auth store', () => {
|
||||
programming: true,
|
||||
design: false,
|
||||
robot: true,
|
||||
cloud_agents: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -310,6 +316,7 @@ describe('auth store', () => {
|
||||
programming: true,
|
||||
design: false,
|
||||
robot: true,
|
||||
cloud_agents: true,
|
||||
},
|
||||
});
|
||||
expect(useAuthStore.getState().isAuthenticated()).toBe(true);
|
||||
@@ -416,6 +423,7 @@ describe('auth store', () => {
|
||||
programming: true,
|
||||
design: false,
|
||||
robot: true,
|
||||
cloud_agents: true,
|
||||
},
|
||||
user: {
|
||||
username: 'zhangsan',
|
||||
@@ -438,6 +446,7 @@ describe('auth store', () => {
|
||||
programming: true,
|
||||
design: true,
|
||||
robot: true,
|
||||
cloud_agents: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -480,6 +489,7 @@ describe('auth store', () => {
|
||||
programming: true,
|
||||
design: true,
|
||||
robot: true,
|
||||
cloud_agents: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -943,6 +953,7 @@ describe('auth store', () => {
|
||||
programming: true,
|
||||
design: true,
|
||||
robot: true,
|
||||
cloud_agents: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
89
tests/unit/cloud-agents-main.test.ts
Normal file
89
tests/unit/cloud-agents-main.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CloudAgentsModule } from '@electron/services/cloud-agents';
|
||||
import { clearWorksSquareSession, storeWorksSquareSession } from '@electron/services/works-square-session';
|
||||
|
||||
const draft = {
|
||||
slug: 'ml-' + 'a'.repeat(32), name: '写作搭档', purpose: '帮助写作', system_prompt: '简明表达',
|
||||
draft_revision: 1, updated_at: '2026-09-10T00:00:00Z',
|
||||
};
|
||||
const input = { operation_id: '12345678-1234-1234-1234-123456789012', name: draft.name, purpose: draft.purpose };
|
||||
const json = (value: unknown, status = 200) => new Response(JSON.stringify(value), { status });
|
||||
const session = () => json({
|
||||
access_token: 'yuxi-secret', token_type: 'bearer', scope: 'makelore-agent-drafts',
|
||||
expires_at: Math.floor(Date.now() / 1000) + 120, api_base_url: 'https://agents.example.test',
|
||||
});
|
||||
function login(key = 'a'.repeat(64)) {
|
||||
storeWorksSquareSession({ accessToken: 'ws-secret', expiresAt: Date.now() + 600000, accountPartitionKey: key });
|
||||
}
|
||||
const instances: CloudAgentsModule[] = [];
|
||||
function moduleFor(fetchImpl: typeof fetch) {
|
||||
const module = new CloudAgentsModule(fetchImpl); instances.push(module); return module;
|
||||
}
|
||||
beforeEach(() => { clearWorksSquareSession(); login(); });
|
||||
afterEach(() => { instances.splice(0).forEach((module) => module.dispose()); clearWorksSquareSession(); vi.useRealTimers(); });
|
||||
|
||||
describe('Main cloud Agents boundary', () => {
|
||||
it('keeps credentials in Main, projects the DTO and sends only creator-independent inputs', async () => {
|
||||
const fetchImpl = vi.fn().mockResolvedValueOnce(session())
|
||||
.mockResolvedValueOnce(json({ ...draft, access_token: 'do-not-project', owner: 'private' }))
|
||||
.mockResolvedValueOnce(json({ agents: [draft], next_cursor: null }));
|
||||
const module = moduleFor(fetchImpl);
|
||||
expect(await module.create({ ...input, payer: 'other', account_id: 'other' })).toEqual(draft);
|
||||
expect(await module.list()).toEqual({ agents: [draft], next_cursor: null });
|
||||
expect(fetchImpl).toHaveBeenCalledTimes(3);
|
||||
expect(fetchImpl.mock.calls[0][1].headers.Authorization).toBe('Bearer ws-secret');
|
||||
expect(fetchImpl.mock.calls[1][1].headers.Authorization).toBe('Bearer yuxi-secret');
|
||||
expect(JSON.parse(fetchImpl.mock.calls[1][1].body)).toEqual(input);
|
||||
expect(fetchImpl.mock.calls[1][1].redirect).toBe('error');
|
||||
});
|
||||
|
||||
it('rejects the old account response after switching accounts', async () => {
|
||||
let finish!: (response: Response) => void;
|
||||
const fetchImpl = vi.fn().mockResolvedValueOnce(session()).mockImplementationOnce(() => new Promise<Response>((resolve) => { finish = resolve; }));
|
||||
const result = moduleFor(fetchImpl).list();
|
||||
const rejection = expect(result).rejects.toMatchObject({ code: 'account_changed' });
|
||||
await vi.waitFor(() => expect(fetchImpl).toHaveBeenCalledTimes(2));
|
||||
login('b'.repeat(64));
|
||||
finish(json({ agents: [draft], next_cursor: null }));
|
||||
await rejection;
|
||||
});
|
||||
|
||||
it('does not automatically repeat a possibly committed create and preserves an explicit retry operation', async () => {
|
||||
const fetchImpl = vi.fn().mockResolvedValueOnce(session()).mockRejectedValueOnce(new Error('lost response ws-secret'))
|
||||
.mockResolvedValueOnce(json(draft));
|
||||
const module = moduleFor(fetchImpl);
|
||||
await expect(module.create(input)).rejects.toMatchObject({ code: 'cloud_service_unavailable' });
|
||||
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
||||
expect(await module.create(input)).toEqual(draft);
|
||||
expect(fetchImpl.mock.calls[2][1].body).toBe(fetchImpl.mock.calls[1][1].body);
|
||||
});
|
||||
|
||||
it('passes revision conflicts without exposing upstream diagnostics', async () => {
|
||||
const fetchImpl = vi.fn().mockResolvedValueOnce(session()).mockResolvedValueOnce(json({
|
||||
detail: { code: 'draft_revision_conflict', message: 'raw yuxi-secret' },
|
||||
}, 409));
|
||||
await expect(moduleFor(fetchImpl).save(draft.slug, {
|
||||
expected_revision: 1, name: draft.name, purpose: draft.purpose, system_prompt: 'new',
|
||||
})).rejects.toMatchObject({ status: 409, code: 'draft_revision_conflict', message: '草稿已在其他设备更新,你的输入已保留' });
|
||||
});
|
||||
|
||||
it('rejects malformed draft responses as service failures', async () => {
|
||||
const fetchImpl = vi.fn().mockResolvedValueOnce(session()).mockResolvedValueOnce(json({ ...draft, draft_revision: 'oops' }));
|
||||
await expect(moduleFor(fetchImpl).get(draft.slug)).rejects.toMatchObject({ status: 502 });
|
||||
});
|
||||
|
||||
it('rejects missing login before any network request', async () => {
|
||||
clearWorksSquareSession();
|
||||
const fetchImpl = vi.fn();
|
||||
await expect(moduleFor(fetchImpl).list()).rejects.toMatchObject({ status: 401 });
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('times out a stalled response body and projects a safe error', async () => {
|
||||
vi.useFakeTimers();
|
||||
const fetchImpl = vi.fn().mockResolvedValueOnce({ text: () => new Promise(() => {}) });
|
||||
const result = expect(moduleFor(fetchImpl).list()).rejects.toMatchObject({ code: 'cloud_service_unavailable' });
|
||||
await vi.advanceTimersByTimeAsync(30000);
|
||||
await result;
|
||||
});
|
||||
});
|
||||
122
tests/unit/cloud-agents-page.test.tsx
Normal file
122
tests/unit/cloud-agents-page.test.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import CloudAgents from '@/pages/CloudAgents';
|
||||
|
||||
const api = vi.hoisted(() => ({ list: vi.fn(), create: vi.fn(), get: vi.fn(), save: vi.fn() }));
|
||||
const auth = vi.hoisted(() => ({ user: { userId: 'creator' } }));
|
||||
vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api }));
|
||||
vi.mock('@/stores/auth', () => ({ useAuthStore: (selector: (state: typeof auth) => unknown) => selector(auth) }));
|
||||
const draft = {
|
||||
slug: 'ml-' + 'a'.repeat(32), name: '写作搭档', purpose: '帮助写作', system_prompt: '原始指令',
|
||||
draft_revision: 1, updated_at: '2026-09-10T00:00:00Z',
|
||||
};
|
||||
function open() {
|
||||
const router = createMemoryRouter([
|
||||
{ path: '/cloud-agents', element: <CloudAgents /> },
|
||||
{ path: '/module-select', element: <div>模块首页</div> },
|
||||
], { initialEntries: ['/cloud-agents'] });
|
||||
const view = render(<RouterProvider router={router} />);
|
||||
return { router, ...view };
|
||||
}
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks(); auth.user.userId = 'creator';
|
||||
api.list.mockResolvedValue({ agents: [draft], next_cursor: null });
|
||||
});
|
||||
afterEach(cleanup);
|
||||
|
||||
it('keeps creation input and its uncertain operation when an existing card is clicked', async () => {
|
||||
api.create.mockRejectedValueOnce(new Error('连接中断')).mockResolvedValueOnce(draft);
|
||||
open();
|
||||
const existing = await screen.findByRole('button', { name: /写作搭档/ });
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建智能体' }));
|
||||
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '新搭档' } });
|
||||
fireEvent.change(screen.getByLabelText('用途'), { target: { value: '新用途' } });
|
||||
fireEvent.click(existing);
|
||||
expect(screen.queryByTestId('cloud-agent-editor')).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText('名称')).toHaveValue('新搭档');
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建草稿' }));
|
||||
await screen.findByRole('alert');
|
||||
fireEvent.click(existing);
|
||||
expect(screen.queryByTestId('cloud-agent-editor')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试创建' }));
|
||||
await screen.findByTestId('cloud-agent-editor');
|
||||
expect(api.create.mock.calls[1][0]).toEqual(api.create.mock.calls[0][0]);
|
||||
});
|
||||
|
||||
it('keeps the same creation operation after a lost response', async () => {
|
||||
api.list.mockResolvedValue({ agents: [], next_cursor: null });
|
||||
api.create.mockRejectedValueOnce(new Error('连接中断')).mockResolvedValueOnce(draft);
|
||||
open();
|
||||
fireEvent.click(await screen.findByText('创建第一个智能体'));
|
||||
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '写作搭档' } });
|
||||
fireEvent.change(screen.getByLabelText('用途'), { target: { value: '帮助写作' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建草稿' }));
|
||||
await screen.findByRole('alert');
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试创建' }));
|
||||
await screen.findByTestId('cloud-agent-editor');
|
||||
expect(api.create).toHaveBeenCalledTimes(2);
|
||||
expect(api.create.mock.calls[1][0]).toEqual(api.create.mock.calls[0][0]);
|
||||
});
|
||||
|
||||
it('preserves local input on conflicts and requires an explicit choice before retry', async () => {
|
||||
api.save.mockRejectedValueOnce(new Error('草稿已更新'));
|
||||
api.get.mockResolvedValue({ ...draft, draft_revision: 2, system_prompt: '其他设备内容' });
|
||||
open();
|
||||
fireEvent.click(await screen.findByRole('button', { name: /写作搭档/ }));
|
||||
fireEvent.change(screen.getByLabelText('角色与指令'), { target: { value: '我的修改' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
|
||||
await screen.findByText('云端已有修订 2,本地输入仍保留');
|
||||
expect(screen.getByLabelText('角色与指令')).toHaveValue('我的修改');
|
||||
expect(screen.getByRole('button', { name: '保存草稿' })).toBeDisabled();
|
||||
fireEvent.click(screen.getByText('保留我的内容,继续编辑'));
|
||||
api.save.mockResolvedValue({ ...draft, draft_revision: 3, system_prompt: '我的修改' });
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }));
|
||||
await screen.findByRole('button', { name: '已保存' });
|
||||
expect(api.save.mock.calls[1][1]).toMatchObject({ expected_revision: 2, system_prompt: '我的修改' });
|
||||
});
|
||||
|
||||
it('blocks module navigation with unsaved edits, cancels leaving, then saves before proceeding', async () => {
|
||||
const { router } = open();
|
||||
fireEvent.click(await screen.findByRole('button', { name: /写作搭档/ }));
|
||||
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '新的名称' } });
|
||||
await act(async () => { await router.navigate('/module-select'); });
|
||||
expect(screen.getByRole('dialog', { name: '未保存的修改' })).toBeVisible();
|
||||
expect(router.state.location.pathname).toBe('/cloud-agents');
|
||||
fireEvent.click(screen.getByText('继续编辑'));
|
||||
expect(screen.getByLabelText('名称')).toHaveValue('新的名称');
|
||||
await act(async () => { await router.navigate('/module-select'); });
|
||||
api.save.mockResolvedValue({ ...draft, name: '新的名称', draft_revision: 2 });
|
||||
fireEvent.click(screen.getByText('保存并离开'));
|
||||
await screen.findByText('模块首页');
|
||||
expect(api.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('clears the previous account workspace and ignores its delayed list', async () => {
|
||||
let finish!: (value: unknown) => void;
|
||||
api.list.mockImplementationOnce(() => new Promise((resolve) => { finish = resolve; }))
|
||||
.mockResolvedValue({ agents: [], next_cursor: null });
|
||||
const view = open();
|
||||
auth.user.userId = 'other';
|
||||
view.rerender(<RouterProvider key="other-account" router={view.router} />);
|
||||
await screen.findByText('你的第一个智能体,从这里开始');
|
||||
await act(async () => finish({ agents: [draft], next_cursor: null }));
|
||||
await waitFor(() => expect(screen.queryByText('写作搭档')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('warns before leaving an uncertain creation and keeps its retry intent', async () => {
|
||||
api.list.mockResolvedValue({ agents: [], next_cursor: null });
|
||||
api.create.mockRejectedValue(new Error('连接中断'));
|
||||
const { router } = open();
|
||||
fireEvent.click(await screen.findByText('创建第一个智能体'));
|
||||
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '写作搭档' } });
|
||||
fireEvent.change(screen.getByLabelText('用途'), { target: { value: '帮助写作' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建草稿' }));
|
||||
await screen.findByRole('alert');
|
||||
await act(async () => { await router.navigate('/module-select'); });
|
||||
expect(screen.getByRole('dialog', { name: '离开创建' })).toBeVisible();
|
||||
fireEvent.click(screen.getByRole('button', { name: '继续创建' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试创建' }));
|
||||
await waitFor(() => expect(api.create).toHaveBeenCalledTimes(2));
|
||||
expect(api.create.mock.calls[1][0]).toEqual(api.create.mock.calls[0][0]);
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getGuardedAiModuleForPath, isAiModuleAllowed, isProgrammingProviderRoute } from '@/lib/ai-modules';
|
||||
import {
|
||||
DEFAULT_MODULE_ACCESS,
|
||||
MODULE_ACCESS_KEYS,
|
||||
@@ -6,12 +7,19 @@ import {
|
||||
} from '../../shared/module-access';
|
||||
|
||||
describe('module access projection', () => {
|
||||
it('contains only the three supported product modules', () => {
|
||||
expect(MODULE_ACCESS_KEYS).toEqual(['programming', 'design', 'robot']);
|
||||
it('honors an explicit cloud Agent denial and does not initialize Code for its route', () => {
|
||||
const access = normalizeModuleAccess({ cloud_agents: false });
|
||||
expect(isAiModuleAllowed('cloud_agents', access)).toBe(false);
|
||||
expect(getGuardedAiModuleForPath('/cloud-agents')).toBe('cloud_agents');
|
||||
expect(isProgrammingProviderRoute('/cloud-agents')).toBe(false);
|
||||
});
|
||||
it('contains only the four supported product modules', () => {
|
||||
expect(MODULE_ACCESS_KEYS).toEqual(['programming', 'design', 'robot', 'cloud_agents']);
|
||||
expect(DEFAULT_MODULE_ACCESS).toEqual({
|
||||
programming: true,
|
||||
design: true,
|
||||
robot: true,
|
||||
cloud_agents: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +35,7 @@ describe('module access projection', () => {
|
||||
programming: false,
|
||||
design: true,
|
||||
robot: false,
|
||||
cloud_agents: true,
|
||||
});
|
||||
expect(access).not.toHaveProperty('learning');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user