feat(consultation): unify server-distributed agents without fixed roles
This commit is contained in:
1 parent
248776d855
commit
8a157be4f6
35 files changed
+625
-668
No files matched your search
@@ -7,10 +7,10 @@ import { TeacherChatPanel } from '@/pages/Chat/TeacherChatPanel';
|
||||
import { TEACHER_CHECK_IN_INTERVAL_MS, type TeacherDefinition, type TeacherTopic } from '../../shared/coding-teacher';
|
||||
|
||||
const api = vi.hoisted(() => ({ config: vi.fn(), list: vi.fn(), read: vi.fn(), create: vi.fn(), send: vi.fn(), events: vi.fn(), checkIn: vi.fn() }));
|
||||
vi.mock('@/lib/coding-teacher', () => ({ teacherApi: api, teacherTopicsPath: (project: string) => project }));
|
||||
vi.mock('@/lib/coding-teacher', () => ({ teacherApi: api, legacyTopicBase: (base: string) => base, teacherTopicsPath: (project: string) => project }));
|
||||
vi.mock('@/stores/auth', () => ({ useAuthStore: (selector: (state: unknown) => unknown) => selector({ user: { userId: 'kid' } }) }));
|
||||
const definition: TeacherDefinition = {
|
||||
schema_version: 1, teacher_id: 'coding-teacher', name: '运营配置的老师', description: '', avatar_id: 'avatar-06', welcome_message: '', suggested_questions: [], system_prompt: '指导', skills: [],
|
||||
schema_version: 1, teacher_id: 'coding-teacher', name: '运营配置的智能体', description: '', avatar_id: 'avatar-06', welcome_message: '', suggested_questions: [], system_prompt: '指导', skills: [],
|
||||
model: { model_id: 'model', reasoning_choice: { mode: 'default' } }, limits: { max_input_tokens: 8000, max_output_tokens: 1500 },
|
||||
};
|
||||
function checkInTopic(projectId = 'project', id = 'check-1'): TeacherTopic {
|
||||
@@ -21,7 +21,7 @@ function Harness({ projectId = 'project', sourceId = 'source', busy = false, arc
|
||||
const [teacherComposing, setTeacherComposing] = useState(false);
|
||||
const companion = useTeacherCompanion({ projectId, sourceId, sourceBusy: busy, sourceArchived: archived, teacherOpen: open, teacherComposing });
|
||||
return <><TeacherCompanion companion={companion} open={open} onOpen={() => setOpen(true)} />
|
||||
{open && <TeacherChatPanel key={projectId} projectId={projectId} sourceId="source" role="teacher" externalTopic={companion.topic} onTopicChange={companion.noteTopic} onComposingChange={setTeacherComposing} onClose={() => setOpen(false)} />}</>;
|
||||
{open && <TeacherChatPanel key={projectId} projectId={projectId} sourceId="source" externalTopic={companion.topic} onTopicChange={companion.noteTopic} onComposingChange={setTeacherComposing} onClose={() => setOpen(false)} />}</>;
|
||||
}
|
||||
async function mount(props: Parameters<typeof Harness>[0] = {}) {
|
||||
let view!: ReturnType<typeof render>;
|
||||
@@ -42,6 +42,29 @@ beforeEach(() => {
|
||||
afterEach(() => { cleanup(); vi.useRealTimers(); });
|
||||
|
||||
describe('teacher presence and periodic guidance', () => {
|
||||
it('shows the selected agent greeting instead of borrowing the default agent greeting', async () => {
|
||||
api.config.mockResolvedValue({ enabled: true, definition: { ...definition, welcome_message: '默认智能体欢迎语' } });
|
||||
const selected = { ...checkInTopic(), requests: [], definition: { ...definition, name: '体验朋友', avatar_id: 'avatar-09', welcome_message: '我们聊聊你的体验' } };
|
||||
api.list.mockResolvedValue({ items: [{ id: selected.id }], lastSelectedTopicId: selected.id });
|
||||
api.read.mockResolvedValue(selected);
|
||||
await mount();
|
||||
expect(screen.getByTestId('teacher-invitation')).toHaveTextContent('我们聊聊你的体验');
|
||||
expect(screen.queryByText('默认智能体欢迎语')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '智能体', exact: true }).querySelector('img')?.src).toContain('avatar-09.png');
|
||||
expect(api.checkIn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not dispatch background turns or subscribe while viewing legacy friend history', async () => {
|
||||
const legacy = { ...checkInTopic(), role: 'friend' as const, requests: [] };
|
||||
api.list.mockResolvedValue({ items: [{ id: legacy.id, legacyRole: 'friend' }], lastSelectedTopicId: legacy.id });
|
||||
api.read.mockResolvedValue(legacy);
|
||||
await mount();
|
||||
await advance();
|
||||
expect(api.checkIn).not.toHaveBeenCalled();
|
||||
expect(api.events).not.toHaveBeenCalled();
|
||||
expect(screen.queryByTestId('teacher-invitation')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the published greeting immediately without a model request and remembers dismissal per project', async () => {
|
||||
api.config.mockResolvedValue({ enabled: true, definition: { ...definition, welcome_message: '有什么编程问题想一起讨论?' } });
|
||||
const view = await mount();
|
||||
@@ -107,10 +130,10 @@ describe('teacher presence and periodic guidance', () => {
|
||||
it('keeps the pixel teacher in the header and only shows its companion avatar with a real message', async () => {
|
||||
await mount();
|
||||
expect(screen.getByText(definition.name)).toBeVisible();
|
||||
const headerAvatar = screen.getByRole('button', { name: '老师', exact: true }).querySelector('img');
|
||||
expect(headerAvatar?.src).toContain('teacher-pixel.png');
|
||||
const headerAvatar = screen.getByRole('button', { name: '智能体', exact: true }).querySelector('img');
|
||||
expect(headerAvatar?.src).toContain('avatar-06.png');
|
||||
expect(screen.queryByTestId('teacher-presence')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('checkbox', { name: '老师偶尔来看看' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('checkbox', { name: '智能体偶尔来看看' })).not.toBeInTheDocument();
|
||||
expect(api.checkIn).not.toHaveBeenCalled();
|
||||
await advance(TEACHER_CHECK_IN_INTERVAL_MS - 1);
|
||||
expect(api.checkIn).not.toHaveBeenCalled();
|
||||
@@ -118,25 +141,25 @@ describe('teacher presence and periodic guidance', () => {
|
||||
expect(api.checkIn).toHaveBeenCalledWith('project', expect.objectContaining({ sourceConversationId: 'source', requestId: expect.any(String) }));
|
||||
expect(screen.getByTestId('teacher-invitation')).toHaveTextContent('排行榜');
|
||||
expect(screen.getByTestId('teacher-presence')).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '打开这条老师消息' }).querySelector('img')?.src).toBe(headerAvatar?.src);
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: '和老师聊聊' })); });
|
||||
expect(screen.getByRole('button', { name: '打开这条智能体消息' }).querySelector('img')?.src).toBe(headerAvatar?.src);
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: '和智能体聊聊' })); });
|
||||
expect(screen.queryByTestId('teacher-invitation')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('teacher-presence')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '老师', exact: true })).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '智能体', exact: true })).toBeVisible();
|
||||
const panel = screen.getByTestId('teacher-chat-panel');
|
||||
expect(within(panel).getByText('你刚才想加排行榜,我们一起想想比什么更有意思?')).toBeVisible();
|
||||
expect(panel.querySelector('.consultation-user')).toBeNull();
|
||||
expect(screen.getByLabelText('向老师提问')).toHaveFocus();
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveFocus();
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
});
|
||||
it('dismisses both the short bubble and its attached portrait without opening a conversation', async () => {
|
||||
await mount(); await advance();
|
||||
const presence = screen.getByTestId('teacher-presence');
|
||||
expect(within(presence).getByTestId('teacher-invitation')).toBeVisible();
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '收起老师气泡' })));
|
||||
await act(async () => fireEvent.click(screen.getByRole('button', { name: '收起智能体气泡' })));
|
||||
expect(screen.queryByTestId('teacher-presence')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('teacher-chat-panel')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '老师', exact: true })).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '智能体', exact: true })).toBeVisible();
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
});
|
||||
it('does not stack invitations and remembers dismissal when the project is reopened', async () => {
|
||||
@@ -153,18 +176,18 @@ describe('teacher presence and periodic guidance', () => {
|
||||
});
|
||||
it('waits for a teacher draft, then keeps the bubble visible even with consultation open until explicitly read', async () => {
|
||||
await mount();
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: '老师', exact: true })); });
|
||||
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '我还在想怎么说' } });
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: '智能体', exact: true })); });
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我还在想怎么说' } });
|
||||
await advance();
|
||||
expect(api.checkIn).not.toHaveBeenCalled();
|
||||
expect(screen.getByLabelText('向老师提问')).toHaveValue('我还在想怎么说');
|
||||
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '' } });
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我还在想怎么说');
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '' } });
|
||||
await advance(15_000);
|
||||
expect(within(screen.getByTestId('teacher-chat-panel')).getByText('你刚才想加排行榜,我们一起想想比什么更有意思?')).toBeVisible();
|
||||
expect(screen.getByTestId('teacher-invitation')).toBeVisible();
|
||||
await advance();
|
||||
expect(api.checkIn).toHaveBeenCalledTimes(1);
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: '看看老师说的' })); });
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: '看看智能体说的' })); });
|
||||
expect(screen.queryByTestId('teacher-invitation')).not.toBeInTheDocument();
|
||||
expect(api.create).not.toHaveBeenCalled();
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
@@ -178,7 +201,7 @@ describe('teacher presence and periodic guidance', () => {
|
||||
expect(screen.getByTestId('teacher-invitation')).toHaveTextContent('排行榜');
|
||||
await advance();
|
||||
expect(api.checkIn).not.toHaveBeenCalled();
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: '老师', exact: true })); });
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: '智能体', exact: true })); });
|
||||
expect(screen.getByTestId('teacher-invitation')).toBeVisible();
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: '等会儿聊' })); });
|
||||
expect(screen.queryByTestId('teacher-invitation')).not.toBeInTheDocument();
|
||||
|
||||
Reference in new issue
Block a user