收敛客户端静态发布链路

This commit is contained in:
2026-08-10 17:04:29 +08:00
parent 2e737dee31
commit 4df047708b
37 changed files with 604 additions and 3071 deletions

View File

@@ -52,14 +52,14 @@ function createContext(deployment: Record<string, unknown> | null, activeProject
path: 'D:/projects/planet-game',
})),
},
worksCloudDeployment: {
worksSubmissionBinding: {
get: vi.fn(async () => deployment),
},
} as never;
}
const submittedDeployment = {
schema_version: 1,
schema_version: 2,
project_id: 'project-1',
status: 'submitted',
requested_at: '2026-08-03T01:00:00.000Z',
@@ -78,7 +78,8 @@ function matchingRemotePayload(overrides: {
project: {
app_id: 'planet-game',
playable: true,
runtime_url: '/apps/planet-game/',
play_url: '/apps/planet-game/',
runtime_url: '/apps/legacy-planet-game/',
version_name: 'v0.7.0',
...overrides.project,
},
@@ -144,7 +145,39 @@ describe('device preview Host API route', () => {
expect(fetchMock).not.toHaveBeenCalled();
});
it('returns only a trusted absolute HTTPS runtime URL for the submitted version', async () => {
it('explains that a legacy automatic deployment must be resubmitted', async () => {
const fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
const response = createResponse();
await handleDevicePreviewRoutes(
createRequest(),
response.res,
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
createContext({
schema_version: 2,
project_id: 'project-1',
status: 'legacy_retired',
requested_at: '2026-08-01T00:00:00.000Z',
updated_at: '2026-08-10T00:00:00.000Z',
error_code: 'LEGACY_AUTO_DEPLOY_RETIRED',
message: '旧版自动部署任务已停用,请在项目配置中点击“提交审核”重新提交。',
}),
);
expect(response.json()).toEqual({
success: true,
preview: {
state: 'unavailable',
projectId: 'project-1',
updatedAt: '2026-08-10T00:00:00.000Z',
message: '旧版自动部署任务已停用,请在项目配置中点击“提交审核”重新提交。',
},
});
expect(fetchMock).not.toHaveBeenCalled();
});
it('prefers the trusted static play URL over the legacy runtime alias', async () => {
const fetchMock = vi.fn().mockResolvedValueOnce(new Response(
JSON.stringify(matchingRemotePayload()),
{ status: 200 },
@@ -185,11 +218,35 @@ describe('device preview Host API route', () => {
expect(JSON.stringify(response.json())).not.toContain('managed-secret-token');
});
it('keeps the legacy runtime alias as a one-release fallback', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(
JSON.stringify(matchingRemotePayload({
project: { play_url: null, runtime_url: '/apps/runtime-fallback/' },
})),
{ status: 200 },
)));
const response = createResponse();
await handleDevicePreviewRoutes(
createRequest(),
response.res,
new URL('http://127.0.0.1/api/opencode/projects/project-1/device-preview'),
createContext(submittedDeployment),
);
expect(response.json()).toMatchObject({
preview: {
state: 'ready',
launchUrl: 'https://square.nianxx.cn/apps/runtime-fallback/',
},
});
});
it('creates an exact short-lived owner preview for a pending-review release', async () => {
const fetchMock = vi.fn()
.mockResolvedValueOnce(new Response(
JSON.stringify(matchingRemotePayload({
project: { playable: false, runtime_url: null },
project: { playable: false, play_url: null, runtime_url: null },
latestVersion: { review_status: 'pending_review' },
})),
{ status: 200 },
@@ -237,7 +294,7 @@ describe('device preview Host API route', () => {
vi.stubGlobal('fetch', vi.fn()
.mockResolvedValueOnce(new Response(
JSON.stringify(matchingRemotePayload({
project: { playable: false, runtime_url: null },
project: { playable: false, play_url: null, runtime_url: null },
latestVersion: { review_status: 'pending_review' },
})),
{ status: 200 },
@@ -262,9 +319,9 @@ describe('device preview Host API route', () => {
it.each([
['an untrusted origin', 'https://evil.example/steal'],
['an overlong URL', `https://square.nianxx.cn/apps/${'x'.repeat(1_100)}`],
])('refuses a runtime URL from %s', async (_case, runtimeUrl) => {
])('refuses a play URL from %s', async (_case, playUrl) => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(
JSON.stringify(matchingRemotePayload({ project: { runtime_url: runtimeUrl } })),
JSON.stringify(matchingRemotePayload({ project: { play_url: playUrl } })),
{ status: 200 },
)));
const response = createResponse();
@@ -445,7 +502,7 @@ describe('device preview Host API route', () => {
const previewUrl = 'https://square.nianxx.cn/apps/planet-game/preview';
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(new Response(
JSON.stringify(matchingRemotePayload({
project: { runtime_url: undefined, preview_url: previewUrl },
project: { play_url: undefined, runtime_url: undefined, preview_url: previewUrl },
})),
{ status: 200 },
)));