feat: integrate learning module
This commit is contained in:
76
tests/unit/learning-agent-client.test.ts
Normal file
76
tests/unit/learning-agent-client.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
// @vitest-environment node
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createLearningAgentClient } from '@electron/services/learning-agent-client';
|
||||
|
||||
describe('Learning Agent client', () => {
|
||||
const fetchImpl = vi.fn<typeof fetch>();
|
||||
const getAccessToken = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
fetchImpl.mockReset();
|
||||
getAccessToken.mockReset();
|
||||
getAccessToken.mockResolvedValue('works-token');
|
||||
});
|
||||
|
||||
it('binds the exact course hash and collects the Learning Runtime answer', async () => {
|
||||
const event = (type: string, payload: Record<string, unknown>) => `event: agent.event\ndata: ${JSON.stringify({ run_id: 'run-1', type, payload })}\n\n`;
|
||||
fetchImpl
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ session_id: 'session-1' }), { status: 201 }))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ run_id: 'run-1' }), { status: 202 }))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ stream_url: '/api/agents/sessions/session-1/events?ticket=ticket-1' }), { status: 200 }))
|
||||
.mockResolvedValueOnce(new Response(
|
||||
event('learning.assistant.delta', { delta: 'Python ' })
|
||||
+ event('learning.assistant.delta', { delta: '很适合入门。' })
|
||||
+ event('learning.assistant.completed', { sources: [] }),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
));
|
||||
const client = createLearningAgentClient({ fetchImpl, getAccessToken, apiBaseUrl: 'https://square.example' });
|
||||
|
||||
await expect(client.ask({
|
||||
courseId: 'course-1',
|
||||
contentHash: 'a'.repeat(64),
|
||||
message: '为什么要学 Python?',
|
||||
anchor: { sceneId: 'scene-1', sceneOrder: 1 },
|
||||
})).resolves.toEqual({ text: 'Python 很适合入门。' });
|
||||
|
||||
expect(JSON.parse(String(fetchImpl.mock.calls[0][1]?.body))).toMatchObject({
|
||||
runtime: 'learning',
|
||||
runtime_version: 'v1',
|
||||
binding: { kind: 'learning-course', key: `course-1:${'a'.repeat(64)}` },
|
||||
});
|
||||
expect(fetchImpl.mock.calls[3][1]?.headers).toEqual({ Accept: 'text/event-stream' });
|
||||
expect(fetchImpl.mock.calls.some((call) => call[1]?.method === 'DELETE')).toBe(false);
|
||||
});
|
||||
|
||||
it('reuses one course-bound Works Agent session across multiple turns', async () => {
|
||||
const stream = (runId: string, answer: string) => new Response(
|
||||
`data: ${JSON.stringify({ run_id: runId, type: 'learning.assistant.delta', payload: { delta: answer } })}\n\n`
|
||||
+ `data: ${JSON.stringify({ run_id: runId, type: 'learning.assistant.completed', payload: {} })}\n\n`,
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
fetchImpl
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ session_id: 'session-shared' }), { status: 201 }))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ run_id: 'run-1' }), { status: 202 }))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ stream_url: '/api/agents/sessions/session-shared/events?ticket=1' }), { status: 200 }))
|
||||
.mockResolvedValueOnce(stream('run-1', '第一轮'))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ run_id: 'run-2' }), { status: 202 }))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ stream_url: '/api/agents/sessions/session-shared/events?ticket=2' }), { status: 200 }))
|
||||
.mockResolvedValueOnce(stream('run-2', '第二轮'));
|
||||
const client = createLearningAgentClient({ fetchImpl, getAccessToken, apiBaseUrl: 'https://square.example' });
|
||||
const binding = { courseId: 'course-1', contentHash: 'a'.repeat(64) };
|
||||
|
||||
await expect(client.ask({ ...binding, message: '第一问' })).resolves.toEqual({ text: '第一轮' });
|
||||
await expect(client.ask({ ...binding, message: '追问' })).resolves.toEqual({ text: '第二轮' });
|
||||
|
||||
const urls = fetchImpl.mock.calls.map(([url]) => String(url));
|
||||
expect(urls.filter((url) => url === 'https://square.example/api/agents/sessions')).toHaveLength(1);
|
||||
expect(urls.filter((url) => url.endsWith('/sessions/session-shared/commands'))).toHaveLength(2);
|
||||
expect(fetchImpl.mock.calls.some((call) => call[1]?.method === 'DELETE')).toBe(false);
|
||||
});
|
||||
|
||||
it('stops before the network for an invalid package hash', async () => {
|
||||
const client = createLearningAgentClient({ fetchImpl, getAccessToken });
|
||||
await expect(client.ask({ courseId: 'course-1', contentHash: 'bad', message: '你好' })).rejects.toThrow('助教请求无效');
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user