在 Makelore 构建并预检静态发布产物
This commit is contained in:
@@ -13,10 +13,31 @@ import {
|
||||
import { createProjectConfig } from '../../shared/project-config';
|
||||
|
||||
const getValidWorksSquareAccessTokenMock = vi.hoisted(() => vi.fn());
|
||||
const prepareProjectReleaseMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@electron/services/works-square-session', () => ({
|
||||
getValidWorksSquareAccessToken: (...args: unknown[]) => getValidWorksSquareAccessTokenMock(...args),
|
||||
}));
|
||||
vi.mock('@electron/services/project-release-builder', async (importOriginal) => ({
|
||||
...await importOriginal<typeof import('@electron/services/project-release-builder')>(),
|
||||
prepareProjectRelease: prepareProjectReleaseMock,
|
||||
}));
|
||||
|
||||
function preparedRelease(projectPath: string) {
|
||||
const bytes = Buffer.from('zip');
|
||||
return {
|
||||
sourceArchive: { path: join(projectPath, 'private-source.zip'), name: 'project.zip', bytes, summary: {
|
||||
archivePath: join(projectPath, 'private-source.zip'), archiveName: 'project.zip', sha256: 'a'.repeat(64), fileCount: 5,
|
||||
sourceBytes: 10, archiveBytes: 3, excludedCount: 0, excludedPaths: [],
|
||||
manifest: { schema_version: 1, project_type: 'mini_game', kind: 'web', runtime: 'static', build: { preset: 'vite', package_manager: 'npm', entry: 'index.html' } },
|
||||
} },
|
||||
builtArchive: { path: join(projectPath, 'private-built.zip'), name: 'built-project.zip', bytes: Buffer.from('built') },
|
||||
distRoot: join(projectPath, 'private-dist'),
|
||||
staticArtifact: { files: [{ path: 'index.html', bytes: Buffer.from('built') }] },
|
||||
contract: { schema_version: 1, entry_path: 'index.html', source_digest: 'a'.repeat(64), built_archive_digest: 'b'.repeat(64), artifact_digest: 'c'.repeat(64), file_count: 1, total_bytes: 5, files: [{ path: 'index.html', size: 5, sha256: 'd'.repeat(64) }], security_profile: 'works-square-static-sandbox-v1', toolchain: { client: 'makelore', client_version: '2.0.0', node: '22', npm: '11.6.2', vite: '7.3.1' } },
|
||||
dispose: vi.fn(async () => undefined),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function createResponse() {
|
||||
@@ -89,6 +110,8 @@ describe('works square host api routes', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
prepareProjectReleaseMock.mockReset();
|
||||
prepareProjectReleaseMock.mockImplementation(async ({ projectPath }: { projectPath: string }) => preparedRelease(projectPath));
|
||||
rotateRendererCapability();
|
||||
getValidWorksSquareAccessTokenMock.mockReset();
|
||||
getValidWorksSquareAccessTokenMock.mockResolvedValue('main-owned-access-token');
|
||||
@@ -1083,7 +1106,9 @@ describe('works square host api routes', () => {
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
const recordSubmitted = vi.fn(async () => undefined);
|
||||
const preflightCurrentProject = vi.fn(async () => ({ ok: true as const }));
|
||||
const preflightStaticArtifact = vi.fn(async () => ({ ok: true as const }));
|
||||
const release = preparedRelease(tempDir);
|
||||
prepareProjectReleaseMock.mockResolvedValueOnce(release);
|
||||
|
||||
const handled = await handleWorksRoutes(
|
||||
createRendererRequest('POST', { projectId: project.id, project: projectMetadata }),
|
||||
@@ -1091,7 +1116,7 @@ describe('works square host api routes', () => {
|
||||
new URL('http://127.0.0.1/api/works/projects/publish-source'),
|
||||
{
|
||||
opencodeProjectStore: { listProjects: vi.fn(async () => [project]) },
|
||||
agentBrowser: { preflightCurrentProject },
|
||||
agentBrowser: { preflightStaticArtifact },
|
||||
worksSubmissionBinding: { recordSubmitted },
|
||||
} as never,
|
||||
);
|
||||
@@ -1149,6 +1174,14 @@ describe('works square host api routes', () => {
|
||||
expect(archive).toBeInstanceOf(File);
|
||||
expect((archive as File).name).toBe('project.zip');
|
||||
expect((archive as File).size).toBeGreaterThan(0);
|
||||
const builtArchive = form.get('built_archive');
|
||||
expect(builtArchive).toBeInstanceOf(File);
|
||||
expect((builtArchive as File).name).toBe('built-project.zip');
|
||||
expect(JSON.parse(String(form.get('artifact_contract')))).toMatchObject({
|
||||
schema_version: 1,
|
||||
source_digest: 'a'.repeat(64),
|
||||
built_archive_digest: 'b'.repeat(64),
|
||||
});
|
||||
expect(recordSubmitted).toHaveBeenCalledWith(project.id, {
|
||||
appId: 'space-cleaner',
|
||||
versionId: 'version-1',
|
||||
@@ -1156,13 +1189,18 @@ describe('works square host api routes', () => {
|
||||
reviewStatus: 'building',
|
||||
zipSha256: expect.stringMatching(/^[a-f0-9]{64}$/),
|
||||
});
|
||||
expect(preflightCurrentProject).toHaveBeenCalledWith(tempDir);
|
||||
expect(preflightStaticArtifact).toHaveBeenCalledWith(release.staticArtifact);
|
||||
expect(response.json()).not.toHaveProperty('contract');
|
||||
expect(JSON.stringify(response.json())).not.toContain('private-dist');
|
||||
expect(JSON.stringify(response.json())).not.toContain('private-source.zip');
|
||||
});
|
||||
|
||||
it('stops before creating or uploading when the local browser preflight fails', async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'makelore-source-preflight-failure-'));
|
||||
await writePublishableProject(tempDir);
|
||||
const preflightCurrentProject = vi.fn(async () => {
|
||||
const release = preparedRelease(tempDir);
|
||||
prepareProjectReleaseMock.mockResolvedValueOnce(release);
|
||||
const preflightStaticArtifact = vi.fn(async () => {
|
||||
throw Object.assign(new Error(`${tempDir} token=secret`), {
|
||||
code: 'PUBLISH_PREFLIGHT_BLANK',
|
||||
});
|
||||
@@ -1186,7 +1224,7 @@ describe('works square host api routes', () => {
|
||||
opencodeProjectStore: {
|
||||
listProjects: vi.fn(async () => [{ id: 'project-1', path: tempDir, name: 'space-cleaner' }]),
|
||||
},
|
||||
agentBrowser: { preflightCurrentProject },
|
||||
agentBrowser: { preflightStaticArtifact },
|
||||
} as never,
|
||||
);
|
||||
|
||||
@@ -1198,10 +1236,31 @@ describe('works square host api routes', () => {
|
||||
error: '作品打开后没有可见内容。',
|
||||
});
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(release.dispose).toHaveBeenCalledOnce();
|
||||
expect(JSON.stringify(response.json())).not.toContain(tempDir);
|
||||
expect(JSON.stringify(response.json())).not.toContain('token=secret');
|
||||
});
|
||||
|
||||
it('stops before preflight or upload and disposes when the local build fails', async () => {
|
||||
prepareProjectReleaseMock.mockRejectedValueOnce(Object.assign(new Error('private path token=secret'), {
|
||||
name: 'ProjectReleaseBuildError',
|
||||
code: 'LOCAL_BUILD_FAILED',
|
||||
}));
|
||||
const preflightStaticArtifact = vi.fn();
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
await handleWorksRoutes(
|
||||
createRendererRequest('POST', { projectId: 'project-1', project: { app_id: 'space-cleaner', title: 'Space', summary: 'Clean' } }),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/projects/publish-source'),
|
||||
{ opencodeProjectStore: { listProjects: vi.fn(async () => [{ id: 'project-1', path: 'private-project' }]) }, agentBrowser: { preflightStaticArtifact } } as never,
|
||||
);
|
||||
expect(preflightStaticArtifact).not.toHaveBeenCalled();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(JSON.stringify(response.json())).not.toContain('token=secret');
|
||||
});
|
||||
|
||||
it('keeps a confirmed submission successful when the local preview mapping cannot be saved', async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'makelore-source-mapping-failure-'));
|
||||
await writePublishableProject(tempDir);
|
||||
@@ -1228,7 +1287,7 @@ describe('works square host api routes', () => {
|
||||
new URL('http://127.0.0.1/api/works/projects/publish-source'),
|
||||
{
|
||||
opencodeProjectStore: { listProjects: vi.fn(async () => [project]) },
|
||||
agentBrowser: { preflightCurrentProject: vi.fn(async () => ({ ok: true })) },
|
||||
agentBrowser: { preflightStaticArtifact: vi.fn(async () => ({ ok: true })) },
|
||||
worksSubmissionBinding: {
|
||||
recordSubmitted: vi.fn(async () => { throw new Error('disk unavailable'); }),
|
||||
},
|
||||
@@ -1275,7 +1334,7 @@ describe('works square host api routes', () => {
|
||||
new URL('http://127.0.0.1/api/works/projects/publish-source'),
|
||||
{
|
||||
opencodeProjectStore: { listProjects: vi.fn(async () => [project]) },
|
||||
agentBrowser: { preflightCurrentProject: vi.fn(async () => ({ ok: true })) },
|
||||
agentBrowser: { preflightStaticArtifact: vi.fn(async () => ({ ok: true })) },
|
||||
} as never,
|
||||
);
|
||||
|
||||
@@ -1306,7 +1365,7 @@ describe('works square host api routes', () => {
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const ctx = {
|
||||
opencodeProjectStore: { listProjects: vi.fn(async () => [project]) },
|
||||
agentBrowser: { preflightCurrentProject: vi.fn(async () => ({ ok: true })) },
|
||||
agentBrowser: { preflightStaticArtifact: vi.fn(async () => ({ ok: true })) },
|
||||
} as never;
|
||||
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
@@ -1363,7 +1422,7 @@ describe('works square host api routes', () => {
|
||||
new URL('http://127.0.0.1/api/works/projects/publish-source'),
|
||||
{
|
||||
opencodeProjectStore: { listProjects: vi.fn(async () => [project]) },
|
||||
agentBrowser: { preflightCurrentProject: vi.fn(async () => ({ ok: true })) },
|
||||
agentBrowser: { preflightStaticArtifact: vi.fn(async () => ({ ok: true })) },
|
||||
} as never,
|
||||
);
|
||||
|
||||
@@ -1402,7 +1461,7 @@ describe('works square host api routes', () => {
|
||||
new URL('http://127.0.0.1/api/works/projects/publish-source'),
|
||||
{
|
||||
opencodeProjectStore: { listProjects: vi.fn(async () => [project]) },
|
||||
agentBrowser: { preflightCurrentProject: vi.fn(async () => ({ ok: true })) },
|
||||
agentBrowser: { preflightStaticArtifact: vi.fn(async () => ({ ok: true })) },
|
||||
} as never,
|
||||
);
|
||||
|
||||
@@ -1418,6 +1477,22 @@ describe('works square host api routes', () => {
|
||||
expect(cancelSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('projects the nested FastAPI client protocol error without exposing its detail', async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'makelore-protocol-required-'));
|
||||
await writePublishableProject(tempDir);
|
||||
const upstream = new Response(JSON.stringify({ detail: { code: 'CLIENT_BUILD_PROTOCOL_REQUIRED', message: 'private token=secret' } }), { status: 422 });
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(upstream));
|
||||
const response = createResponse();
|
||||
await handleWorksRoutes(
|
||||
createRendererRequest('POST', { projectId: 'project-1', project: { app_id: 'space-cleaner', title: 'Space', summary: 'Clean' } }),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/projects/publish-source'),
|
||||
{ opencodeProjectStore: { listProjects: vi.fn(async () => [{ id: 'project-1', path: tempDir }]) }, agentBrowser: { preflightStaticArtifact: vi.fn(async () => ({ ok: true })) } } as never,
|
||||
);
|
||||
expect(response.json()).toEqual({ success: false, status: 422, code: 'CLIENT_BUILD_PROTOCOL_REQUIRED', error: '请升级 Makelore 并重新提交。' });
|
||||
expect(JSON.stringify(response.json())).not.toContain('token=secret');
|
||||
});
|
||||
|
||||
it('fails safely before packaging when the Main session is unavailable', async () => {
|
||||
getValidWorksSquareAccessTokenMock.mockResolvedValueOnce(null);
|
||||
const fetchMock = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user