Makelore 2.0 initial clean snapshot

This commit is contained in:
inman
2026-07-29 17:22:35 +08:00
commit b8ca3f8eea
694 changed files with 139782 additions and 0 deletions

View File

@@ -0,0 +1,292 @@
import { EventEmitter } from 'node:events';
import type { IncomingMessage, ServerResponse } from 'node:http';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { handleMeowaGameAssetsRoutes } from '@electron/api/routes/meowa-game-assets';
const getApiKeyMock = vi.hoisted(() => vi.fn());
const storeApiKeyMock = vi.hoisted(() => vi.fn());
const deleteApiKeyMock = vi.hoisted(() => vi.fn());
const readBundledMeowaApiKeyMock = vi.hoisted(() => vi.fn());
const readEmbeddedMeowaApiKeyMock = vi.hoisted(() => vi.fn());
const proxyAwareFetchMock = vi.hoisted(() => vi.fn());
vi.mock('@electron/utils/secure-storage', () => ({
getApiKey: (...args: unknown[]) => getApiKeyMock(...args),
storeApiKey: (...args: unknown[]) => storeApiKeyMock(...args),
deleteApiKey: (...args: unknown[]) => deleteApiKeyMock(...args),
}));
vi.mock('@electron/services/meowa-game-assets-release-credential', () => ({
readBundledMeowaApiKey: (...args: unknown[]) => readBundledMeowaApiKeyMock(...args),
readEmbeddedMeowaApiKey: (...args: unknown[]) => readEmbeddedMeowaApiKeyMock(...args),
}));
vi.mock('@electron/utils/proxy-fetch', () => ({
proxyAwareFetch: (...args: unknown[]) => proxyAwareFetchMock(...args),
}));
function createResponse() {
const chunks: string[] = [];
const response = {
statusCode: 0,
setHeader: vi.fn(),
end: vi.fn((chunk?: string) => {
if (chunk) chunks.push(chunk);
}),
} as unknown as ServerResponse;
return {
response,
json: () => JSON.parse(chunks.join('')) as Record<string, unknown>,
};
}
function createRequest(method: string, body?: unknown): IncomingMessage {
const request = new EventEmitter();
Object.assign(request, {
method,
headers: body === undefined ? {} : { 'content-type': 'application/json' },
[Symbol.asyncIterator]: async function* () {
if (body !== undefined) yield Buffer.from(JSON.stringify(body));
},
});
return request as IncomingMessage;
}
describe('Meowa game-assets Host API routes', () => {
beforeEach(() => {
vi.clearAllMocks();
delete process.env.MEOWART_API_KEY;
getApiKeyMock.mockResolvedValue(null);
readBundledMeowaApiKeyMock.mockResolvedValue(null);
readEmbeddedMeowaApiKeyMock.mockReturnValue(null);
storeApiKeyMock.mockResolvedValue(true);
deleteApiKeyMock.mockResolvedValue(true);
});
it('reports configuration without returning the stored credential', async () => {
getApiKeyMock.mockResolvedValue('meowa-secret');
const { response, json } = createResponse();
await handleMeowaGameAssetsRoutes(
createRequest('GET'),
response,
new URL('http://127.0.0.1/api/meowa/game-assets/config'),
{} as never,
);
expect(response.statusCode).toBe(200);
expect(json()).toMatchObject({
configured: true,
credentialSource: 'secure-store',
provider: 'meowa',
});
expect(json()).not.toHaveProperty('quota');
expect(JSON.stringify(json())).not.toContain('meowa-secret');
});
it('hydrates and reports a packaged release credential without returning it', async () => {
readBundledMeowaApiKeyMock.mockResolvedValue('meowa-release-secret');
const { response, json } = createResponse();
await handleMeowaGameAssetsRoutes(
createRequest('GET'),
response,
new URL('http://127.0.0.1/api/meowa/game-assets/config'),
{} as never,
);
expect(response.statusCode).toBe(200);
expect(json()).toMatchObject({
configured: true,
credentialSource: 'release-bundle',
});
expect(JSON.stringify(json())).not.toContain('meowa-release-secret');
expect(storeApiKeyMock).toHaveBeenCalledWith('meowa-game-assets', 'meowa-release-secret');
});
it('persists and reports the embedded credential without returning it', async () => {
readEmbeddedMeowaApiKeyMock.mockReturnValue('embedded-meowa-secret');
const { response, json } = createResponse();
await handleMeowaGameAssetsRoutes(
createRequest('GET'),
response,
new URL('http://127.0.0.1/api/meowa/game-assets/config'),
{} as never,
);
expect(response.statusCode).toBe(200);
expect(json()).toMatchObject({ configured: true, credentialSource: 'embedded' });
expect(JSON.stringify(json())).not.toContain('embedded-meowa-secret');
expect(storeApiKeyMock).toHaveBeenCalledWith('meowa-game-assets', 'embedded-meowa-secret');
});
it('prefers the packaged release credential over the embedded fallback', async () => {
readBundledMeowaApiKeyMock.mockResolvedValue('release-secret');
readEmbeddedMeowaApiKeyMock.mockReturnValue('embedded-secret');
const { response, json } = createResponse();
await handleMeowaGameAssetsRoutes(
createRequest('GET'),
response,
new URL('http://127.0.0.1/api/meowa/game-assets/config'),
{} as never,
);
expect(json()).toMatchObject({ credentialSource: 'release-bundle' });
expect(storeApiKeyMock).toHaveBeenCalledWith('meowa-game-assets', 'release-secret');
expect(readEmbeddedMeowaApiKeyMock).not.toHaveBeenCalled();
});
it('does not expose the removed local quota route', async () => {
getApiKeyMock.mockResolvedValue('meowa-secret');
const { response } = createResponse();
const handled = await handleMeowaGameAssetsRoutes(
createRequest('GET'),
response,
new URL('http://127.0.0.1/api/meowa/game-assets/quota'),
{} as never,
);
expect(handled).toBe(false);
expect(response.end).not.toHaveBeenCalled();
});
it('proxies template info with the stored credential kept in Main', async () => {
getApiKeyMock.mockResolvedValue('meowa-secret');
proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({ templates: [{ name: 'pixel-character' }] }), {
status: 200,
headers: { 'content-type': 'application/json' },
}));
const { response, json } = createResponse();
await handleMeowaGameAssetsRoutes(
createRequest('GET'),
response,
new URL('http://127.0.0.1/api/meowa/game-assets/template-info?kind=pixel'),
{} as never,
);
expect(response.statusCode).toBe(200);
expect(json()).toEqual({ templates: [{ name: 'pixel-character' }] });
expect(proxyAwareFetchMock).toHaveBeenCalledWith(
'https://api.meowa.ai/api/pixel-gen/template-info',
expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer meowa-secret' }) }),
);
});
it('converts generation JSON into Meowa multipart form data', async () => {
getApiKeyMock.mockResolvedValue('meowa-secret');
proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({ api_job_id: 'job-1' }), {
status: 202,
headers: { 'content-type': 'application/json' },
}));
const { response, json } = createResponse();
await handleMeowaGameAssetsRoutes(
createRequest('POST', {
kind: 'pixel',
templateName: 'pixel-character',
requirement: '透明背景的像素角色',
templateConfig: { size: 64 },
}),
response,
new URL('http://127.0.0.1/api/meowa/game-assets/generate'),
{} as never,
);
expect(response.statusCode).toBe(202);
expect(json()).toEqual({ api_job_id: 'job-1' });
const [, init] = proxyAwareFetchMock.mock.calls[0] as [string, RequestInit];
const form = init.body as FormData;
expect(form.get('template_name')).toBe('pixel-character');
expect(form.get('requirement')).toBe('透明背景的像素角色');
expect(form.get('template_config')).toBe('{"size":64}');
expect(JSON.stringify([...form.entries()])).not.toContain('meowa-secret');
});
it('forwards generation without a local daily quota check', async () => {
getApiKeyMock.mockResolvedValue('meowa-secret');
proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({ api_job_id: 'job-after-ten' }), {
status: 202,
headers: { 'content-type': 'application/json' },
}));
const { response, json } = createResponse();
await handleMeowaGameAssetsRoutes(
createRequest('POST', {
kind: 'pixel',
templateName: 'pixel-character',
requirement: '透明背景的像素角色',
}),
response,
new URL('http://127.0.0.1/api/meowa/game-assets/generate'),
{} as never,
);
expect(response.statusCode).toBe(202);
expect(json()).toEqual({ api_job_id: 'job-after-ten' });
expect(proxyAwareFetchMock).toHaveBeenCalledOnce();
});
it('forwards Meowa rejection when a generation submission fails', async () => {
getApiKeyMock.mockResolvedValue('meowa-secret');
proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({ error: 'invalid template' }), {
status: 400,
headers: { 'content-type': 'application/json' },
}));
const { response } = createResponse();
await handleMeowaGameAssetsRoutes(
createRequest('POST', {
kind: 'pixel',
templateName: 'pixel-character',
requirement: '透明背景的像素角色',
}),
response,
new URL('http://127.0.0.1/api/meowa/game-assets/generate'),
{} as never,
);
expect(response.statusCode).toBe(400);
});
it('downloads bytes through Main without exposing a signed upstream URL', async () => {
getApiKeyMock.mockResolvedValue('meowa-secret');
proxyAwareFetchMock.mockResolvedValue(new Response(Buffer.from('png-bytes'), {
status: 200,
headers: { 'content-type': 'image/png' },
}));
const { response, json } = createResponse();
await handleMeowaGameAssetsRoutes(
createRequest('GET'),
response,
new URL('http://127.0.0.1/api/meowa/game-assets/download?kind=pixel&id=job-1'),
{} as never,
);
expect(response.statusCode).toBe(200);
expect(json()).toMatchObject({ success: true, mimeType: 'image/png', dataBase64: Buffer.from('png-bytes').toString('base64') });
expect(JSON.stringify(json())).not.toContain('meowa-secret');
});
it('blocks generation before network access when no credential is configured', async () => {
const { response, json } = createResponse();
await handleMeowaGameAssetsRoutes(
createRequest('POST', { kind: 'pixel', templateName: 'pixel-character', requirement: 'test' }),
response,
new URL('http://127.0.0.1/api/meowa/game-assets/generate'),
{} as never,
);
expect(response.statusCode).toBe(409);
expect(json()).toMatchObject({
code: 'MEOWA_API_KEY_MISSING',
error: 'Meowa 素材服务尚未配置,请联系管理员。',
});
expect(proxyAwareFetchMock).not.toHaveBeenCalled();
});
});