feat: 将发布预检移入内置浏览器
This commit is contained in:
57
tests/e2e/local-preview-preflight.spec.ts
Normal file
57
tests/e2e/local-preview-preflight.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { createServer } from 'node:http';
|
||||
import { test, expect } from './fixtures/electron';
|
||||
|
||||
test('the production local preview preflight isolates requests and cleans up its temporary renderers', async ({ electronApp }) => {
|
||||
let externalRequests = 0;
|
||||
let externalPageLoads = 0;
|
||||
const externalServer = createServer((_request, response) => {
|
||||
externalRequests += 1;
|
||||
response.writeHead(200, { 'Content-Type': 'application/javascript' });
|
||||
response.end('globalThis.externalLoaded = true;');
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
externalServer.once('error', reject);
|
||||
externalServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
const externalAddress = externalServer.address();
|
||||
if (!externalAddress || typeof externalAddress === 'string') throw new Error('Expected an ephemeral TCP port');
|
||||
|
||||
const server = createServer((request, response) => {
|
||||
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
if (request.url === '/external') externalPageLoads += 1;
|
||||
const externalScript = request.url === '/external' && externalPageLoads > 1
|
||||
? `<script src="http://127.0.0.1:${externalAddress.port}/blocked.js"></script>`
|
||||
: '';
|
||||
response.end(`<!doctype html><meta name="viewport" content="width=device-width,initial-scale=1"><main style="width:100vw;height:100vh">Playable</main>${externalScript}`);
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') throw new Error('Expected an ephemeral TCP port');
|
||||
|
||||
try {
|
||||
const initialWebContentsCount = await electronApp.evaluate(({ webContents }) => webContents.getAllWebContents().length);
|
||||
const runPreflight = async (url: string) => await electronApp.evaluate(async (_electron, targetUrl) => {
|
||||
const mainGlobal = globalThis as typeof globalThis & {
|
||||
__niancodeRunLocalPreviewPreflightE2E?: (value: string) => Promise<{ ok: true }>;
|
||||
};
|
||||
if (!mainGlobal.__niancodeRunLocalPreviewPreflightE2E) throw new Error('E2E preflight seam is unavailable');
|
||||
return await mainGlobal.__niancodeRunLocalPreviewPreflightE2E(targetUrl);
|
||||
}, url);
|
||||
|
||||
await expect(runPreflight(`http://127.0.0.1:${address.port}/external`)).rejects.toThrow();
|
||||
expect(externalRequests).toBe(0);
|
||||
|
||||
const result = await runPreflight(`http://127.0.0.1:${address.port}/`);
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
await expect.poll(
|
||||
async () => await electronApp.evaluate(({ webContents }) => webContents.getAllWebContents().length),
|
||||
).toBe(initialWebContentsCount);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
await new Promise<void>((resolve) => externalServer.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
@@ -116,6 +116,7 @@ class FakeWebContents implements AgentBrowserWebContentsPort {
|
||||
windowOpenDenied = false;
|
||||
onLoad?: () => void;
|
||||
loadHandler?: (url: string) => Promise<void>;
|
||||
evaluateHandler?: (code: string) => Promise<unknown>;
|
||||
|
||||
async loadURL(url: string): Promise<void> {
|
||||
this.debugger.operationLog.push(`load:${url}`);
|
||||
@@ -145,6 +146,17 @@ class FakeWebContents implements AgentBrowserWebContentsPort {
|
||||
this.reloadCalls += 1;
|
||||
}
|
||||
|
||||
async executeJavaScript(code: string): Promise<unknown> {
|
||||
const overridden = this.debugger.commands.findLast(
|
||||
(command) => command.method === 'Emulation.setDeviceMetricsOverride',
|
||||
)?.params;
|
||||
return await (this.evaluateHandler?.(code) ?? Promise.resolve({
|
||||
readyState: 'complete',
|
||||
visible: true,
|
||||
viewport: { width: overridden?.width, height: overridden?.height },
|
||||
}));
|
||||
}
|
||||
|
||||
denyWindowOpen(): void {
|
||||
this.windowOpenDenied = true;
|
||||
}
|
||||
@@ -185,6 +197,8 @@ class FakeAdapter implements AgentBrowserAdapter {
|
||||
destroyed = 0;
|
||||
onCreate?: (view: FakeView) => void;
|
||||
resetHandler?: (partition: string) => Promise<void>;
|
||||
destroyHandler?: () => void;
|
||||
readonly restrictedOrigins: Array<{ partition: string; origin: string; released: boolean }> = [];
|
||||
|
||||
createView(partition: string): AgentBrowserViewPort {
|
||||
const view = new FakeView();
|
||||
@@ -205,12 +219,21 @@ class FakeAdapter implements AgentBrowserAdapter {
|
||||
destroy(view: AgentBrowserViewPort): void {
|
||||
this.destroyed += 1;
|
||||
(view.webContents as FakeWebContents).destroyed = true;
|
||||
this.destroyHandler?.();
|
||||
}
|
||||
|
||||
async resetPartition(partition: string): Promise<void> {
|
||||
this.resetPartitions.push(partition);
|
||||
await this.resetHandler?.(partition);
|
||||
}
|
||||
|
||||
restrictPartitionToOrigin(partition: string, origin: string): () => void {
|
||||
const record = { partition, origin, released: false };
|
||||
this.restrictedOrigins.push(record);
|
||||
return () => {
|
||||
record.released = true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const projectPath = 'D:\\student\\clock';
|
||||
@@ -229,6 +252,180 @@ async function openBrowser(adapter = new FakeAdapter()) {
|
||||
}
|
||||
|
||||
describe('AgentBrowserModule', () => {
|
||||
it('preflights desktop and mobile viewports in temporary non-persistent profiles', async () => {
|
||||
const adapter = new FakeAdapter();
|
||||
adapter.onCreate = (view) => {
|
||||
view.webContents.evaluateHandler = async () => {
|
||||
const metrics = view.webContents.debugger.commands.findLast(
|
||||
(command) => command.method === 'Emulation.setDeviceMetricsOverride',
|
||||
)?.params;
|
||||
return {
|
||||
readyState: 'complete',
|
||||
visible: true,
|
||||
viewport: { width: metrics?.width, height: metrics?.height },
|
||||
};
|
||||
};
|
||||
};
|
||||
const module = new AgentBrowserModule(adapter);
|
||||
|
||||
await module.open({ projectId: 'clock', projectPath, url: 'http://127.0.0.1:4173/' });
|
||||
await expect(module.preflightCurrentProject(projectPath)).resolves.toEqual({ ok: true });
|
||||
|
||||
expect(adapter.partitions).toHaveLength(3);
|
||||
expect(adapter.partitions.slice(1).every((partition) => partition.startsWith('niancode-publish-preflight:'))).toBe(true);
|
||||
expect(adapter.partitions.slice(1).every((partition) => !partition.startsWith('persist:'))).toBe(true);
|
||||
expect(adapter.views.slice(1).map((view) => view.bounds)).toEqual([
|
||||
{ x: 0, y: 0, width: 1280, height: 720 },
|
||||
{ x: 0, y: 0, width: 390, height: 844 },
|
||||
]);
|
||||
expect(adapter.mounted).toBe(1);
|
||||
expect(adapter.destroyed).toBe(2);
|
||||
expect(adapter.resetPartitions).toEqual(adapter.partitions.slice(1));
|
||||
expect(adapter.restrictedOrigins).toEqual(adapter.partitions.slice(1).map((partition) => ({
|
||||
partition,
|
||||
origin: 'http://127.0.0.1:4173',
|
||||
released: true,
|
||||
})));
|
||||
});
|
||||
|
||||
it.each([
|
||||
['console error', (view: FakeView) => view.webContents.debugger.message('Runtime.consoleAPICalled', { type: 'error' })],
|
||||
['page exception', (view: FakeView) => view.webContents.debugger.message('Runtime.exceptionThrown', { exceptionDetails: {} })],
|
||||
['log error', (view: FakeView) => view.webContents.debugger.message('Log.entryAdded', { entry: { level: 'error' } })],
|
||||
['failed response', (view: FakeView) => view.webContents.debugger.message('Network.responseReceived', { response: { status: 404 } })],
|
||||
['external request', (view: FakeView) => view.webContents.debugger.message('Network.requestWillBeSent', { request: { url: 'https://evil.example/track' } })],
|
||||
])('fails and cleans up a publish preflight on %s', async (_case, trigger) => {
|
||||
const adapter = new FakeAdapter();
|
||||
adapter.onCreate = (view) => {
|
||||
view.webContents.evaluateHandler = async () => {
|
||||
trigger(view);
|
||||
const metrics = view.webContents.debugger.commands.findLast(
|
||||
(command) => command.method === 'Emulation.setDeviceMetricsOverride',
|
||||
)?.params;
|
||||
return { readyState: 'complete', visible: true, viewport: { width: metrics?.width, height: metrics?.height } };
|
||||
};
|
||||
};
|
||||
const module = new AgentBrowserModule(adapter);
|
||||
|
||||
await module.open({ projectId: 'clock', projectPath, url: 'http://127.0.0.1:4173/' });
|
||||
await expect(module.preflightCurrentProject(projectPath)).rejects.toMatchObject({
|
||||
code: 'PUBLISH_PREFLIGHT_RUNTIME_ERROR',
|
||||
});
|
||||
expect(adapter.destroyed).toBe(1);
|
||||
expect(adapter.resetPartitions).toEqual(adapter.partitions.slice(1));
|
||||
});
|
||||
|
||||
it('ignores a cancelled navigation resource instead of reporting a runtime error', async () => {
|
||||
const adapter = new FakeAdapter();
|
||||
adapter.onCreate = (view) => {
|
||||
if (adapter.views.length === 1) return;
|
||||
view.webContents.evaluateHandler = async () => {
|
||||
view.webContents.debugger.message('Network.loadingFailed', {
|
||||
canceled: true,
|
||||
errorText: 'net::ERR_ABORTED',
|
||||
});
|
||||
const metrics = view.webContents.debugger.commands.findLast(
|
||||
(command) => command.method === 'Emulation.setDeviceMetricsOverride',
|
||||
)?.params;
|
||||
return { readyState: 'complete', visible: true, viewport: { width: metrics?.width, height: metrics?.height } };
|
||||
};
|
||||
};
|
||||
const module = new AgentBrowserModule(adapter);
|
||||
await module.open({ projectId: 'clock', projectPath, url: 'http://127.0.0.1:4173/' });
|
||||
|
||||
await expect(module.preflightCurrentProject(projectPath)).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('rejects a top-level redirect to another origin', async () => {
|
||||
const adapter = new FakeAdapter();
|
||||
adapter.onCreate = (view) => {
|
||||
if (adapter.views.length === 1) return;
|
||||
view.webContents.evaluateHandler = async () => {
|
||||
view.webContents.url = 'http://127.0.0.1:4174/redirected';
|
||||
return { readyState: 'complete', visible: true };
|
||||
};
|
||||
};
|
||||
const module = new AgentBrowserModule(adapter);
|
||||
await module.open({ projectId: 'clock', projectPath, url: 'http://127.0.0.1:4173/' });
|
||||
|
||||
await expect(module.preflightCurrentProject(projectPath)).rejects.toMatchObject({
|
||||
code: 'PUBLISH_PREFLIGHT_LOAD_FAILED',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a white screen without exposing page content and still cleans up', async () => {
|
||||
const adapter = new FakeAdapter();
|
||||
adapter.onCreate = (view) => {
|
||||
view.webContents.evaluateHandler = async () => ({
|
||||
readyState: 'complete',
|
||||
visible: false,
|
||||
privateText: 'C:\\private\\student-project',
|
||||
});
|
||||
};
|
||||
const module = new AgentBrowserModule(adapter);
|
||||
|
||||
await module.open({ projectId: 'clock', projectPath, url: 'http://127.0.0.1:4173/' });
|
||||
await expect(module.preflightCurrentProject(projectPath)).rejects.toMatchObject({
|
||||
code: 'PUBLISH_PREFLIGHT_BLANK',
|
||||
message: '作品打开后没有可见内容。',
|
||||
});
|
||||
expect(adapter.destroyed).toBe(1);
|
||||
expect(adapter.resetPartitions).toEqual(adapter.partitions.slice(1));
|
||||
});
|
||||
|
||||
it('continues cleanup when destroy and origin-release fail', async () => {
|
||||
const adapter = new FakeAdapter();
|
||||
adapter.destroyHandler = () => {
|
||||
throw new Error('destroy failed');
|
||||
};
|
||||
adapter.onCreate = (view) => {
|
||||
view.webContents.evaluateHandler = async () => ({ readyState: 'complete', visible: false });
|
||||
};
|
||||
const originalRestrict = adapter.restrictPartitionToOrigin.bind(adapter);
|
||||
adapter.restrictPartitionToOrigin = (partition, origin) => {
|
||||
originalRestrict(partition, origin);
|
||||
return () => {
|
||||
throw new Error('release failed');
|
||||
};
|
||||
};
|
||||
const module = new AgentBrowserModule(adapter);
|
||||
await module.open({ projectId: 'clock', projectPath, url: 'http://127.0.0.1:4173/' });
|
||||
|
||||
await expect(module.preflightCurrentProject(projectPath)).rejects.toMatchObject({
|
||||
code: 'PUBLISH_PREFLIGHT_BLANK',
|
||||
});
|
||||
expect(adapter.destroyed).toBe(1);
|
||||
expect(adapter.resetPartitions).toEqual(adapter.partitions.slice(1));
|
||||
});
|
||||
|
||||
it.each([
|
||||
'https://example.com:443/',
|
||||
'https://localhost:4173/',
|
||||
'http://localhost/',
|
||||
'http://user:secret@127.0.0.1:4173/',
|
||||
])('requires an explicit credential-free loopback preview URL: %s', async (url) => {
|
||||
const adapter = new FakeAdapter();
|
||||
const module = new AgentBrowserModule(adapter);
|
||||
await module.open({ projectId: 'clock', projectPath, url });
|
||||
|
||||
await expect(module.preflightCurrentProject(projectPath)).rejects.toMatchObject({
|
||||
code: 'PREVIEW_REQUIRED',
|
||||
});
|
||||
expect(adapter.views).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('requires the current project preview and leaves the user browser record untouched', async () => {
|
||||
const { adapter, module, snapshot, view } = await openBrowser();
|
||||
const before = await module.getSnapshot(projectPath);
|
||||
|
||||
await expect(module.preflightCurrentProject('D:\\student\\other')).rejects.toMatchObject({
|
||||
code: 'PREVIEW_REQUIRED',
|
||||
});
|
||||
expect(await module.getSnapshot(projectPath)).toEqual(before);
|
||||
expect(view.webContents.getURL()).toBe(snapshot.url);
|
||||
expect(adapter.views).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('primes the renderer before attaching CDP and loading the shared page', async () => {
|
||||
const adapter = new FakeAdapter();
|
||||
adapter.onCreate = (view) => {
|
||||
|
||||
@@ -40,6 +40,7 @@ const electronMocks = vi.hoisted(() => {
|
||||
setPermissionCheckHandler: permissionCheckHandler,
|
||||
clearStorageData: vi.fn().mockResolvedValue(undefined),
|
||||
clearCache: vi.fn().mockResolvedValue(undefined),
|
||||
webRequest: { onBeforeRequest: vi.fn() },
|
||||
});
|
||||
|
||||
class MockWebContentsView {
|
||||
@@ -64,6 +65,7 @@ const electronMocks = vi.hoisted(() => {
|
||||
isDestroyed: vi.fn().mockReturnValue(false),
|
||||
isDevToolsOpened: vi.fn().mockReturnValue(false),
|
||||
reload: vi.fn(),
|
||||
executeJavaScript: vi.fn().mockResolvedValue({ ok: true }),
|
||||
close: vi.fn(),
|
||||
setWindowOpenHandler: vi.fn(),
|
||||
});
|
||||
@@ -100,7 +102,7 @@ describe('ElectronAgentBrowserAdapter', () => {
|
||||
electronMocks.browserSession.removeAllListeners();
|
||||
});
|
||||
|
||||
it('creates an isolated sandboxed WebContentsView and mounts it in Main', () => {
|
||||
it('creates an isolated sandboxed WebContentsView and mounts it in Main', async () => {
|
||||
const addChildView = vi.fn();
|
||||
const removeChildView = vi.fn();
|
||||
const mainWindow = {
|
||||
@@ -139,6 +141,7 @@ describe('ElectronAgentBrowserAdapter', () => {
|
||||
view.setBounds({ x: 100, y: 80, width: 800, height: 600 });
|
||||
view.setVisible(true);
|
||||
view.webContents.navigationHistory.clear();
|
||||
await expect(view.webContents.executeJavaScript('document.readyState')).resolves.toEqual({ ok: true });
|
||||
expect(electronMocks.nativeViews[0].setBounds).toHaveBeenCalledWith({
|
||||
x: 125,
|
||||
y: 100,
|
||||
@@ -208,4 +211,34 @@ describe('ElectronAgentBrowserAdapter', () => {
|
||||
expect(customRedirect.preventDefault).toHaveBeenCalledOnce();
|
||||
expect(webNavigation.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cancels cross-origin web requests before they leave a temporary partition', () => {
|
||||
const adapter = new ElectronAgentBrowserAdapter({
|
||||
isDestroyed: vi.fn().mockReturnValue(false),
|
||||
contentView: { addChildView: vi.fn(), removeChildView: vi.fn() },
|
||||
webContents: { getZoomFactor: vi.fn().mockReturnValue(1) },
|
||||
getContentBounds: vi.fn().mockReturnValue({ x: 0, y: 0, width: 1024, height: 768 }),
|
||||
} as never);
|
||||
const release = adapter.restrictPartitionToOrigin(
|
||||
'niancode-publish-preflight:test',
|
||||
'http://127.0.0.1:4173',
|
||||
);
|
||||
const listener = electronMocks.browserSession.webRequest.onBeforeRequest.mock.calls[0]?.[1];
|
||||
const sameOrigin = vi.fn();
|
||||
const external = vi.fn();
|
||||
const websocket = vi.fn();
|
||||
const secureWebsocket = vi.fn();
|
||||
|
||||
listener({ url: 'http://127.0.0.1:4173/app.js' }, sameOrigin);
|
||||
listener({ url: 'https://evil.example/track' }, external);
|
||||
listener({ url: 'ws://127.0.0.1:4173/socket' }, websocket);
|
||||
listener({ url: 'wss://127.0.0.1:4173/socket' }, secureWebsocket);
|
||||
release();
|
||||
|
||||
expect(sameOrigin).toHaveBeenCalledWith({ cancel: false });
|
||||
expect(external).toHaveBeenCalledWith({ cancel: true });
|
||||
expect(websocket).toHaveBeenCalledWith({ cancel: true });
|
||||
expect(secureWebsocket).toHaveBeenCalledWith({ cancel: true });
|
||||
expect(electronMocks.browserSession.webRequest.onBeforeRequest).toHaveBeenLastCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,6 +82,10 @@ describe('ProjectPublishAction', () => {
|
||||
expect(screen.getByRole('button', { name: '正在检查并提交…' })).toBeDisabled();
|
||||
await flushSubmission();
|
||||
|
||||
expect(screen.getByTestId('project-publish-status')).toHaveTextContent(
|
||||
'本地预览检查已完成,项目已上传,正在等待云端受控构建与平台校验。',
|
||||
);
|
||||
|
||||
expect(publishWorksProjectSourceMock).toHaveBeenCalledWith({
|
||||
projectId: 'prj_space_cleaner',
|
||||
project: {
|
||||
|
||||
@@ -6,6 +6,9 @@ import {
|
||||
|
||||
describe('works project publish guidance', () => {
|
||||
it.each([
|
||||
['PREVIEW_REQUIRED', '请先打开项目预览', '内置浏览器'],
|
||||
['PUBLISH_PREFLIGHT_RUNTIME_ERROR', '作品打开时发生错误', '项目预览'],
|
||||
['PUBLISH_PREFLIGHT_BLANK', '作品打开后没有内容', '移动端布局'],
|
||||
['PROJECT_FILE_MISSING', '项目文件不完整', '修复当前项目模板'],
|
||||
['PROJECT_TYPE_UNPUBLISHABLE', '这个项目没有配置发布方式', '新建小游戏或小程序项目'],
|
||||
['DEPENDENCY_PREFETCH_FAILED', '暂时无法下载项目依赖', 'package-lock.json'],
|
||||
|
||||
@@ -18,6 +18,7 @@ vi.mock('@electron/services/works-square-session', () => ({
|
||||
getValidWorksSquareAccessToken: (...args: unknown[]) => getValidWorksSquareAccessTokenMock(...args),
|
||||
}));
|
||||
|
||||
|
||||
function createResponse() {
|
||||
const chunks: string[] = [];
|
||||
const res = {
|
||||
@@ -1082,6 +1083,7 @@ 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 handled = await handleWorksRoutes(
|
||||
createRendererRequest('POST', { projectId: project.id, project: projectMetadata }),
|
||||
@@ -1089,6 +1091,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 },
|
||||
worksSubmissionBinding: { recordSubmitted },
|
||||
} as never,
|
||||
);
|
||||
@@ -1153,6 +1156,50 @@ describe('works square host api routes', () => {
|
||||
reviewStatus: 'building',
|
||||
zipSha256: expect.stringMatching(/^[a-f0-9]{64}$/),
|
||||
});
|
||||
expect(preflightCurrentProject).toHaveBeenCalledWith(tempDir);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
throw Object.assign(new Error(`${tempDir} token=secret`), {
|
||||
code: 'PUBLISH_PREFLIGHT_BLANK',
|
||||
});
|
||||
});
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
|
||||
await handleWorksRoutes(
|
||||
createRendererRequest('POST', {
|
||||
projectId: 'project-1',
|
||||
project: {
|
||||
app_id: 'space-cleaner',
|
||||
title: '太空清洁队',
|
||||
summary: '收集漂浮垃圾的小游戏。',
|
||||
},
|
||||
}),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/projects/publish-source'),
|
||||
{
|
||||
opencodeProjectStore: {
|
||||
listProjects: vi.fn(async () => [{ id: 'project-1', path: tempDir, name: 'space-cleaner' }]),
|
||||
},
|
||||
agentBrowser: { preflightCurrentProject },
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({
|
||||
success: false,
|
||||
status: 400,
|
||||
code: 'PUBLISH_PREFLIGHT_BLANK',
|
||||
error: '作品打开后没有可见内容。',
|
||||
});
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(JSON.stringify(response.json())).not.toContain(tempDir);
|
||||
expect(JSON.stringify(response.json())).not.toContain('token=secret');
|
||||
});
|
||||
|
||||
it('keeps a confirmed submission successful when the local preview mapping cannot be saved', async () => {
|
||||
@@ -1181,6 +1228,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 })) },
|
||||
worksSubmissionBinding: {
|
||||
recordSubmitted: vi.fn(async () => { throw new Error('disk unavailable'); }),
|
||||
},
|
||||
@@ -1225,7 +1273,10 @@ describe('works square host api routes', () => {
|
||||
}),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/projects/publish-source'),
|
||||
{ opencodeProjectStore: { listProjects: vi.fn(async () => [project]) } } as never,
|
||||
{
|
||||
opencodeProjectStore: { listProjects: vi.fn(async () => [project]) },
|
||||
agentBrowser: { preflightCurrentProject: vi.fn(async () => ({ ok: true })) },
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(response.json().success).toBe(true);
|
||||
@@ -1253,7 +1304,10 @@ describe('works square host api routes', () => {
|
||||
.mockRejectedValueOnce(new Error('socket reset with secret upstream detail'))
|
||||
.mockResolvedValueOnce(new Response(uploadPayload, { status: 201 }));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const ctx = { opencodeProjectStore: { listProjects: vi.fn(async () => [project]) } } as never;
|
||||
const ctx = {
|
||||
opencodeProjectStore: { listProjects: vi.fn(async () => [project]) },
|
||||
agentBrowser: { preflightCurrentProject: vi.fn(async () => ({ ok: true })) },
|
||||
} as never;
|
||||
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
const response = createResponse();
|
||||
@@ -1307,7 +1361,10 @@ describe('works square host api routes', () => {
|
||||
}),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/projects/publish-source'),
|
||||
{ opencodeProjectStore: { listProjects: vi.fn(async () => [project]) } } as never,
|
||||
{
|
||||
opencodeProjectStore: { listProjects: vi.fn(async () => [project]) },
|
||||
agentBrowser: { preflightCurrentProject: vi.fn(async () => ({ ok: true })) },
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
@@ -1343,7 +1400,10 @@ describe('works square host api routes', () => {
|
||||
}),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/works/projects/publish-source'),
|
||||
{ opencodeProjectStore: { listProjects: vi.fn(async () => [project]) } } as never,
|
||||
{
|
||||
opencodeProjectStore: { listProjects: vi.fn(async () => [project]) },
|
||||
agentBrowser: { preflightCurrentProject: vi.fn(async () => ({ ok: true })) },
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
Reference in New Issue
Block a user