merge: master into codex/bark-battle

This commit is contained in:
kdletters
2026-05-19 17:04:32 +08:00
307 changed files with 40711 additions and 26022 deletions

View File

@@ -1561,6 +1561,65 @@ function buildMockPuzzleAgentSession(
};
}
function buildReadyPuzzleDraft(
overrides: Partial<PuzzleResultDraft> = {},
): PuzzleResultDraft {
return {
workTitle: '自动恢复拼图',
workDescription: '前端断连后复读 session 恢复的拼图。',
levelName: '雨夜猫街',
summary: '屋檐下的猫与暖灯街角。',
themeTags: ['猫咪', '雨夜', '拼图'],
forbiddenDirectives: [],
creatorIntent: null,
anchorPack: buildPuzzleAnchorPack(),
candidates: [
{
candidateId: 'candidate-1',
imageSrc: '/puzzle/recovered-candidate.png',
assetId: 'asset-1',
prompt: '雨夜猫街',
actualPrompt: null,
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'candidate-1',
coverImageSrc: '/puzzle/recovered-candidate.png',
coverAssetId: 'asset-1',
generationStatus: 'ready',
levels: [
{
levelId: 'puzzle-level-1',
levelName: '雨夜猫街',
pictureDescription: '屋檐下的猫与暖灯街角。',
pictureReference: null,
candidates: [
{
candidateId: 'candidate-1',
imageSrc: '/puzzle/recovered-candidate.png',
assetId: 'asset-1',
prompt: '雨夜猫街',
actualPrompt: null,
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'candidate-1',
coverImageSrc: '/puzzle/recovered-candidate.png',
coverAssetId: 'asset-1',
uiBackgroundPrompt: '雨夜猫街竖屏纯背景',
uiBackgroundImageSrc:
'/generated-puzzle-assets/puzzle-session-recovered/ui/background.png',
uiBackgroundImageObjectKey:
'generated-puzzle-assets/puzzle-session-recovered/ui/background.png',
generationStatus: 'ready',
},
],
...overrides,
};
}
function buildClearedPuzzleRun(params: {
runId: string;
entryProfileId: string;
@@ -1626,6 +1685,20 @@ function buildMockMatch3DRun(profileId: string): Match3DRunSnapshot {
};
}
const match3DGeneratedUiAsset = {
prompt: '果园竖屏纯背景',
imageSrc: '/generated-match3d-assets/session/profile/background/background.png',
imageObjectKey:
'generated-match3d-assets/session/profile/background/background.png',
containerPrompt: '果园浅盘容器',
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/container.png',
containerImageObjectKey:
'generated-match3d-assets/session/profile/ui-container/container.png',
status: 'image_ready',
error: null,
} satisfies NonNullable<Match3DWorkSummary['generatedBackgroundAsset']>;
function buildMockMatch3DAgentSession(
overrides: Partial<Match3DAgentSessionSnapshot> = {},
): Match3DAgentSessionSnapshot {
@@ -3415,6 +3488,73 @@ test('running match3d persisted draft reopens progress instead of unfinished res
);
});
test('persisted generating match3d draft opens generation progress after refresh', async () => {
const user = userEvent.setup();
const persistedGeneratingWork: Match3DWorkSummary = {
workId: 'match3d-work-generating',
profileId: 'match3d-profile-generating',
ownerUserId: 'user-1',
sourceSessionId: 'match3d-session-generating',
gameName: '生成中抓鹅',
themeText: '霓虹水果摊',
summary: '刷新后仍应回到抓大鹅生成面板。',
tags: ['水果', '抓大鹅'],
coverImageSrc: null,
referenceImageSrc: null,
clearCount: 12,
difficulty: 4,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-18T12:05:00.000Z',
publishedAt: null,
publishReady: false,
generationStatus: 'generating',
generatedItemAssets: [],
};
vi.mocked(listMatch3DWorks).mockResolvedValue({
items: [persistedGeneratingWork],
});
vi.mocked(match3dCreationClient.getSession).mockResolvedValueOnce({
session: buildMockMatch3DAgentSession({
sessionId: 'match3d-session-generating',
draft: {
profileId: 'match3d-profile-generating',
gameName: '生成中抓鹅',
themeText: '霓虹水果摊',
summary: '刷新后仍应回到抓大鹅生成面板。',
tags: ['水果', '抓大鹅'],
coverImageSrc: null,
referenceImageSrc: null,
clearCount: 12,
difficulty: 4,
generatedItemAssets: [],
},
stage: 'draft_ready',
lastAssistantReply: '正在生成抓大鹅素材。',
updatedAt: '2026-05-18T12:05:00.000Z',
}),
});
render(<TestWrapper withAuth />);
await openDraftHub(user);
await user.click(
await screen.findByRole('button', { name: /继续创作《生成中抓鹅》/u }),
);
await waitFor(() => {
expect(match3dCreationClient.getSession).toHaveBeenCalledWith(
'match3d-session-generating',
);
});
expect(await screen.findByText('抓大鹅草稿生成进度')).toBeTruthy();
expect(screen.queryByText('抓大鹅结果页')).toBeNull();
expect(getMatch3DWorkDetail).not.toHaveBeenCalledWith(
'match3d-profile-generating',
);
});
test('running match3d form generation keeps other creation templates available', async () => {
const user = userEvent.setup();
const runningSession = buildMockMatch3DAgentSession({
@@ -3885,6 +4025,7 @@ test('match3d result trial passes generated models into first runtime mount', as
subscriptionKey: 'sub-strawberry',
status: 'model_ready',
error: null,
backgroundAsset: match3DGeneratedUiAsset,
},
];
const match3dDraftWork: Match3DWorkSummary = {
@@ -4132,6 +4273,7 @@ test('match3d draft generation auto starts trial and runtime back opens draft re
subscriptionKey: 'sub-strawberry',
status: 'model_ready',
error: null,
backgroundAsset: match3DGeneratedUiAsset,
},
];
const generatedSession = buildMockMatch3DAgentSession({
@@ -4195,6 +4337,26 @@ test('match3d draft generation auto starts trial and runtime back opens draft re
expect(
await screen.findByTestId('match3d-runtime-generated-model-count'),
).toHaveProperty('textContent', '1');
await waitFor(() => {
expect(
screen.getByTestId('match3d-runtime-top-level-background-count'),
).toHaveProperty('textContent', '1');
});
expect(
screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
).toHaveProperty('textContent', '1');
expect(
match3dGeneratedModelCache.preloadMatch3DGeneratedRuntimeAssets,
).toHaveBeenCalledWith(
expect.any(Array),
expect.objectContaining({
imageSrc:
'/generated-match3d-assets/session/profile/background/background.png',
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/container.png',
}),
{ expireSeconds: 300 },
);
await user.click(screen.getByRole('button', { name: '返回' }));
@@ -4202,6 +4364,110 @@ test('match3d draft generation auto starts trial and runtime back opens draft re
expect(screen.getByText('自动试玩抓大鹅')).toBeTruthy();
});
test('match3d result trial loads generated background and container assets', async () => {
const user = userEvent.setup();
const generatedItemAssets: Match3DWorkSummary['generatedItemAssets'] = [
{
itemId: 'match3d-trial-item-1',
itemName: '草莓',
imageSrc:
'/generated-match3d-assets/session/profile/items/match3d-trial-item-1-item/image.png',
imageObjectKey:
'generated-match3d-assets/session/profile/items/match3d-trial-item-1-item/image.png',
imageViews: [],
modelSrc: null,
modelObjectKey: null,
modelFileName: null,
taskUuid: null,
subscriptionKey: null,
status: 'image_ready',
error: null,
backgroundAsset: match3DGeneratedUiAsset,
},
];
const match3dDraftWork: Match3DWorkSummary = {
workId: 'match3d-work-trial-ui',
profileId: 'match3d-profile-trial-ui',
ownerUserId: 'user-1',
sourceSessionId: 'match3d-session-trial-ui',
gameName: '手动试玩抓大鹅',
themeText: '水果',
summary: '',
tags: ['水果', '抓大鹅'],
coverImageSrc: null,
referenceImageSrc: null,
clearCount: 12,
difficulty: 4,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-14T11:00:00.000Z',
publishedAt: null,
publishReady: false,
generatedBackgroundAsset: null,
generatedItemAssets,
};
vi.mocked(listMatch3DWorks).mockResolvedValue({
items: [match3dDraftWork],
});
vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
session: buildMockMatch3DAgentSession({
sessionId: 'match3d-session-trial-ui',
stage: 'draft_ready',
draft: {
profileId: 'match3d-profile-trial-ui',
gameName: '手动试玩抓大鹅',
themeText: '水果',
summary: '',
tags: ['水果', '抓大鹅'],
coverImageSrc: null,
referenceImageSrc: null,
clearCount: 12,
difficulty: 4,
generatedItemAssets,
},
}),
});
vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
item: match3dDraftWork,
});
match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
run: buildMockMatch3DRun(match3dDraftWork.profileId),
});
render(<TestWrapper withAuth />);
await openDraftHub(user);
await user.click(
await screen.findByRole('button', { name: /继续创作《手动试玩抓大鹅》/u }),
);
expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '试玩' }));
expect(await screen.findByText(/抓大鹅运行态/u)).toBeTruthy();
await waitFor(() => {
expect(
screen.getByTestId('match3d-runtime-top-level-background-count'),
).toHaveProperty('textContent', '1');
});
expect(
screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
).toHaveProperty('textContent', '1');
expect(
match3dGeneratedModelCache.preloadMatch3DGeneratedRuntimeAssets,
).toHaveBeenCalledWith(
expect.any(Array),
expect.objectContaining({
imageSrc:
'/generated-match3d-assets/session/profile/background/background.png',
containerImageSrc:
'/generated-match3d-assets/session/profile/ui-container/container.png',
}),
{ expireSeconds: 300 },
);
});
test('completed match3d draft notice first opens trial then reopens result', async () => {
const user = userEvent.setup();
const generatedItemAssets: Match3DWorkSummary['generatedItemAssets'] = [
@@ -4220,6 +4486,7 @@ test('completed match3d draft notice first opens trial then reopens result', asy
subscriptionKey: 'sub-notice-strawberry',
status: 'image_ready',
error: null,
backgroundAsset: match3DGeneratedUiAsset,
},
];
const runningSession = buildMockMatch3DAgentSession({
@@ -4313,6 +4580,14 @@ test('completed match3d draft notice first opens trial then reopens result', asy
expect(await screen.findByText(/抓大鹅运行态/u)).toBeTruthy();
expect(screen.queryByText('抓大鹅草稿生成进度')).toBeNull();
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(
screen.getByTestId('match3d-runtime-top-level-background-count'),
).toHaveProperty('textContent', '1');
});
expect(
screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
).toHaveProperty('textContent', '1');
await user.click(screen.getByRole('button', { name: '返回' }));
expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '返回' }));
@@ -4439,51 +4714,14 @@ test('completed baby object match draft shows unread marker after leaving genera
test('puzzle draft generation auto starts trial and runtime back opens draft result', async () => {
const user = userEvent.setup();
const generatedDraft: PuzzleResultDraft = {
const generatedDraft = buildReadyPuzzleDraft({
workTitle: '自动试玩拼图',
workDescription: '生成完成后直接试玩。',
levelName: '雨夜猫街',
summary: '屋檐下的猫与暖灯街角。',
themeTags: ['猫咪', '雨夜', '拼图'],
forbiddenDirectives: [],
creatorIntent: null,
anchorPack: buildPuzzleAnchorPack(),
candidates: [
{
candidateId: 'candidate-1',
imageSrc: '/puzzle/auto-candidate.png',
assetId: 'asset-1',
prompt: '雨夜猫街',
actualPrompt: null,
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'candidate-1',
coverImageSrc: '/puzzle/auto-candidate.png',
coverAssetId: 'asset-1',
generationStatus: 'ready',
levels: [
{
levelId: 'puzzle-level-1',
levelName: '雨夜猫街',
pictureDescription: '屋檐下的猫与暖灯街角。',
pictureReference: null,
candidates: [
{
candidateId: 'candidate-1',
imageSrc: '/puzzle/auto-candidate.png',
assetId: 'asset-1',
prompt: '雨夜猫街',
actualPrompt: null,
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'candidate-1',
...buildReadyPuzzleDraft().levels![0]!,
coverImageSrc: '/puzzle/auto-candidate.png',
coverAssetId: 'asset-1',
uiBackgroundPrompt: '水果乐园竖屏纯背景',
uiBackgroundImageSrc:
'/generated-puzzle-assets/puzzle-session-auto-1/ui/background.png',
uiBackgroundImageObjectKey:
@@ -4499,10 +4737,9 @@ test('puzzle draft generation auto starts trial and runtime back opens draft res
title: '水果乐园',
updatedAt: '2026-05-14T10:00:00.000Z',
},
generationStatus: 'ready',
},
],
};
});
const generatedSession: PuzzleAgentSessionSnapshot = {
sessionId: 'puzzle-session-auto-1',
seedText: '屋檐下的猫与暖灯街角。',
@@ -4586,6 +4823,63 @@ test('puzzle draft generation auto starts trial and runtime back opens draft res
expect(screen.getByDisplayValue('雨夜猫街')).toBeTruthy();
});
test('embedded puzzle form recovers when compile request times out after backend completion', async () => {
const user = userEvent.setup();
const generatedDraft = buildReadyPuzzleDraft();
const generatedSession = buildMockPuzzleAgentSession({
sessionId: 'puzzle-session-recovered',
stage: 'ready_to_publish',
progressPercent: 100,
draft: generatedDraft,
lastAssistantReply: '拼图草稿已经生成。',
resultPreview: {
draft: generatedDraft,
publishReady: true,
blockers: [],
qualityFindings: [],
},
updatedAt: '2026-05-12T10:00:00.000Z',
});
vi.mocked(createPuzzleAgentSession).mockResolvedValueOnce({
session: buildMockPuzzleAgentSession({
sessionId: 'puzzle-session-recovered',
}),
});
vi.mocked(executePuzzleAgentAction).mockRejectedValueOnce(
Object.assign(new Error('请求超时90000ms'), {
name: 'TimeoutError',
}),
);
vi.mocked(getPuzzleAgentSession).mockResolvedValueOnce({
session: generatedSession,
});
render(<TestWrapper withAuth />);
await openCreateTemplateHub(user);
await user.click(screen.getByRole('button', { name: '生成草稿' }));
await waitFor(() => {
expect(getPuzzleAgentSession).toHaveBeenCalledWith(
'puzzle-session-recovered',
);
});
await waitFor(() => {
expect(updatePuzzleWork).toHaveBeenCalledWith(
'puzzle-profile-recovered',
expect.objectContaining({
levelName: '雨夜猫街',
coverImageSrc: '/puzzle/recovered-candidate.png',
}),
);
});
expect(screen.queryByText('执行拼图操作失败。')).toBeNull();
expect(screen.queryByText('请求超时90000ms')).toBeNull();
expect(screen.queryByText('拼图草稿生成进度')).toBeNull();
expect(startLocalPuzzleRun).toHaveBeenCalledTimes(1);
});
test('embedded puzzle form routes through requireAuth while logged out', async () => {
const user = userEvent.setup();
const requireAuth = vi.fn();
@@ -5657,6 +5951,12 @@ test('home recommendation Match3D runtime keeps image, music and UI assets witho
'textContent',
'1',
);
expect(
screen.getByTestId('match3d-runtime-top-level-background-count'),
).toHaveProperty('textContent', '1');
expect(
screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
).toHaveProperty('textContent', '1');
});
test('home recommendation Match3D runtime passes top-level UI background assets', async () => {
@@ -5830,6 +6130,12 @@ test('home recommendation Match3D runtime reloads detail when card only has UI a
expect(
await screen.findByTestId('match3d-runtime-generated-item-image-count'),
).toHaveProperty('textContent', '1');
expect(
screen.getByTestId('match3d-runtime-top-level-background-count'),
).toHaveProperty('textContent', '1');
expect(
screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
).toHaveProperty('textContent', '1');
});
test('home recommendation surfaces start failure instead of staying in loading state', async () => {
@@ -6227,6 +6533,59 @@ test('puzzle draft result back button returns to creation hub', async () => {
expect(screen.queryByText('拼图结果页')).toBeNull();
});
test('persisted generating puzzle draft opens generation progress after refresh', async () => {
const user = userEvent.setup();
vi.mocked(listPuzzleWorks).mockResolvedValue({
items: [
{
workId: 'puzzle-work-session-generating',
profileId: 'puzzle-profile-session-generating',
ownerUserId: 'user-1',
sourceSessionId: 'puzzle-session-generating',
authorDisplayName: '测试玩家',
workTitle: '生成中拼图',
workDescription: '刷新后仍应回到生成面板。',
levelName: '生成中拼图',
summary: '刷新后仍应回到生成面板。',
themeTags: ['雨夜'],
coverImageSrc: null,
coverAssetId: null,
publicationStatus: 'draft',
updatedAt: '2026-05-18T12:00:00.000Z',
publishedAt: null,
playCount: 0,
remixCount: 0,
likeCount: 0,
publishReady: false,
generationStatus: 'generating',
},
],
});
vi.mocked(getPuzzleAgentSession).mockResolvedValueOnce({
session: buildMockPuzzleAgentSession({
sessionId: 'puzzle-session-generating',
stage: 'collecting_anchors',
progressPercent: 42,
lastAssistantReply: '正在生成拼图草稿。',
updatedAt: '2026-05-18T12:00:00.000Z',
}),
});
render(<TestWrapper withAuth />);
await openDraftHub(user);
await user.click(await screen.findByRole('button', { name: /继续创作/u }));
await waitFor(() => {
expect(getPuzzleAgentSession).toHaveBeenCalledWith(
'puzzle-session-generating',
);
});
expect(await screen.findByText('拼图草稿生成进度')).toBeTruthy();
expect(screen.queryByText('拼图结果页')).toBeNull();
});
test('published puzzle work card restores its source session for editing', async () => {
const user = userEvent.setup();