feat: integrate learning module
This commit is contained in:
179
tests/unit/learning-route.test.ts
Normal file
179
tests/unit/learning-route.test.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import { Readable } from 'node:stream';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createLearningRouteHandler } from '@electron/api/routes/learning';
|
||||
|
||||
function createResponse() {
|
||||
const chunks: string[] = [];
|
||||
const res = {
|
||||
statusCode: 0,
|
||||
setHeader: vi.fn(),
|
||||
end: vi.fn((chunk?: string) => {
|
||||
if (chunk) chunks.push(chunk);
|
||||
}),
|
||||
} as unknown as ServerResponse;
|
||||
return {
|
||||
res,
|
||||
get json() {
|
||||
return JSON.parse(chunks.join('')) as Record<string, unknown>;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('Learning Main route boundary', () => {
|
||||
const fetchImpl = vi.fn<typeof fetch>();
|
||||
const getAccessToken = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
fetchImpl.mockReset();
|
||||
getAccessToken.mockReset();
|
||||
});
|
||||
|
||||
it('does not claim unrelated routes', async () => {
|
||||
const handler = createLearningRouteHandler({ fetchImpl, getAccessToken });
|
||||
const response = createResponse();
|
||||
|
||||
await expect(handler(
|
||||
{ method: 'GET' } as IncomingMessage,
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/projects'),
|
||||
{} as never,
|
||||
)).resolves.toBe(false);
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('forwards catalog reads with a Main-owned access token', async () => {
|
||||
getAccessToken.mockResolvedValue('works-token');
|
||||
fetchImpl.mockResolvedValue(new Response(JSON.stringify([]), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}));
|
||||
const handler = createLearningRouteHandler({
|
||||
fetchImpl,
|
||||
getAccessToken,
|
||||
apiBaseUrl: 'https://square.example/',
|
||||
});
|
||||
const response = createResponse();
|
||||
|
||||
await expect(handler(
|
||||
{ method: 'GET' } as IncomingMessage,
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/learning/courses?limit=24&unknown=no'),
|
||||
{} as never,
|
||||
)).resolves.toBe(true);
|
||||
|
||||
expect(fetchImpl).toHaveBeenCalledWith(
|
||||
'https://square.example/api/learning/courses?limit=24',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer works-token',
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(response.json).toEqual({ success: true, data: [] });
|
||||
});
|
||||
|
||||
it('refreshes once after an upstream 401', async () => {
|
||||
getAccessToken.mockResolvedValueOnce('expired-token').mockResolvedValueOnce('new-token');
|
||||
fetchImpl
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ detail: 'expired' }), { status: 401 }))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify([]), { status: 200 }));
|
||||
const handler = createLearningRouteHandler({ fetchImpl, getAccessToken, apiBaseUrl: 'https://square.example' });
|
||||
const response = createResponse();
|
||||
|
||||
await handler(
|
||||
{ method: 'GET' } as IncomingMessage,
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/learning/courses'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(getAccessToken).toHaveBeenNthCalledWith(2, { fetchImpl, forceRefresh: true });
|
||||
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
||||
expect(response.json).toMatchObject({ success: true });
|
||||
});
|
||||
|
||||
it('returns a stable auth error without contacting the service', async () => {
|
||||
getAccessToken.mockResolvedValue(null);
|
||||
const handler = createLearningRouteHandler({ fetchImpl, getAccessToken });
|
||||
const response = createResponse();
|
||||
|
||||
await handler(
|
||||
{ method: 'GET' } as IncomingMessage,
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/learning/courses'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(response.res.statusCode).toBe(401);
|
||||
expect(response.json).toMatchObject({ code: 'LEARNING_AUTH_REQUIRED' });
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('forwards single-course generation without exposing the token to Renderer', async () => {
|
||||
getAccessToken.mockResolvedValue('works-token');
|
||||
fetchImpl.mockResolvedValue(new Response(JSON.stringify({
|
||||
jobId: 'job-1', status: 'queued', done: false,
|
||||
}), { status: 202, headers: { 'Content-Type': 'application/json' } }));
|
||||
const handler = createLearningRouteHandler({
|
||||
fetchImpl,
|
||||
getAccessToken,
|
||||
apiBaseUrl: 'https://square.example',
|
||||
});
|
||||
const options = {
|
||||
requirement: '互动讲解二次函数',
|
||||
enableWebSearch: false,
|
||||
enableImageGeneration: true,
|
||||
enableVideoGeneration: false,
|
||||
enableTTS: true,
|
||||
interactiveMode: true,
|
||||
taskEngineMode: false,
|
||||
};
|
||||
const request = Readable.from([JSON.stringify(options)]) as IncomingMessage;
|
||||
request.method = 'POST';
|
||||
const response = createResponse();
|
||||
|
||||
await handler(
|
||||
request,
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/learning/generations'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(fetchImpl).toHaveBeenCalledWith(
|
||||
'https://square.example/api/learning/generations',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify(options),
|
||||
headers: expect.objectContaining({ Authorization: 'Bearer works-token' }),
|
||||
}),
|
||||
);
|
||||
expect(response.res.statusCode).toBe(202);
|
||||
});
|
||||
|
||||
it.each(['cancel', 'resume'])('forwards the generation %s control operation without a body', async (operation) => {
|
||||
getAccessToken.mockResolvedValue('works-token');
|
||||
fetchImpl.mockResolvedValue(new Response(JSON.stringify({
|
||||
jobId: 'job-1',
|
||||
status: operation === 'cancel' ? 'cancelled' : 'queued',
|
||||
done: operation === 'cancel',
|
||||
}), { status: 200 }));
|
||||
const handler = createLearningRouteHandler({ fetchImpl, getAccessToken, apiBaseUrl: 'https://square.example' });
|
||||
const response = createResponse();
|
||||
|
||||
await handler(
|
||||
{ method: 'POST' } as IncomingMessage,
|
||||
response.res,
|
||||
new URL(`http://127.0.0.1/api/works/learning/generations/job-1/${operation}`),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(fetchImpl).toHaveBeenCalledWith(
|
||||
`https://square.example/api/learning/generations/job-1/${operation}`,
|
||||
expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Authorization: 'Bearer works-token' }) }),
|
||||
);
|
||||
expect(fetchImpl.mock.calls[0][1]).not.toHaveProperty('body');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user