Files
makelore/tests/unit/learning-agent-client.test.ts
inman 01bee3188b
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
feat: integrate learning module
2026-08-16 20:52:16 +08:00

77 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @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();
});
});