This commit is contained in:
2026-05-02 17:56:42 +08:00
parent 2311edb2e6
commit acc55d0e13
40 changed files with 2582 additions and 931 deletions

View File

@@ -15,6 +15,13 @@ import type {
} from '../../../packages/shared/src/contracts/match3dAgent';
import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type {
PuzzleAnchorPack,
PuzzleResultDraft,
} from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type {
PuzzleAgentSessionSnapshot,
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
@@ -619,6 +626,41 @@ function buildMockPuzzleRun(
};
}
function buildPuzzleAnchorPack(): PuzzleAnchorPack {
return {
themePromise: {
key: 'themePromise',
label: '题材承诺',
value: '雨夜拼图',
status: 'confirmed',
},
visualSubject: {
key: 'visualSubject',
label: '画面主体',
value: '雨夜猫塔',
status: 'confirmed',
},
visualMood: {
key: 'visualMood',
label: '视觉气质',
value: '暖灯',
status: 'confirmed',
},
compositionHooks: {
key: 'compositionHooks',
label: '拼图记忆点',
value: '灯塔与猫',
status: 'confirmed',
},
tagsAndForbidden: {
key: 'tagsAndForbidden',
label: '标签与禁忌',
value: '雨夜、猫咪、塔',
status: 'confirmed',
},
};
}
function buildClearedPuzzleRun(params: {
runId: string;
entryProfileId: string;
@@ -2198,6 +2240,54 @@ test('logged out public detail gates puzzle start and remix before real actions'
expect(remixPuzzleGalleryWork).not.toHaveBeenCalled();
});
test('owned public puzzle detail edits original draft instead of remixing', async () => {
const user = userEvent.setup();
const ownedPuzzleWork = {
workId: 'puzzle-work-owned-1',
profileId: 'puzzle-profile-owned-1',
ownerUserId: mockAuthUser.id,
sourceSessionId: 'puzzle-session-1',
authorDisplayName: mockAuthUser.displayName,
levelName: '星桥机关',
summary: '旋转碎片并接通星桥机关。',
themeTags: ['机关', '星桥'],
coverImageSrc: null,
coverAssetId: null,
publicationStatus: 'published',
updatedAt: '2026-04-25T09:00:00.000Z',
publishedAt: '2026-04-25T09:00:00.000Z',
playCount: 3,
remixCount: 0,
likeCount: 0,
publishReady: true,
} satisfies PuzzleWorkSummary;
vi.mocked(listPuzzleGallery).mockResolvedValue({
items: [ownedPuzzleWork],
});
vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
item: ownedPuzzleWork,
});
render(<TestWrapper withAuth />);
await waitFor(() => {
expect(screen.getAllByText('星桥机关').length).toBeGreaterThan(0);
});
const workCards = screen.getAllByRole('button', { name: //u });
await user.click(workCards[0]!);
expect(await screen.findByText('详情')).toBeTruthy();
expect(screen.getByRole('button', { name: '作品编辑' })).toBeTruthy();
expect(screen.queryByRole('button', { name: '作品改造' })).toBeNull();
await user.click(screen.getByRole('button', { name: '作品编辑' }));
expect(getPuzzleAgentSession).toHaveBeenCalledWith('puzzle-session-1');
expect(remixPuzzleGalleryWork).not.toHaveBeenCalled();
expect(await screen.findByText('拼图结果页')).toBeTruthy();
});
test('logged out public detail gates big fish start before local runtime', async () => {
const user = userEvent.setup();
const requireAuth = vi.fn();
@@ -2496,6 +2586,13 @@ test('published puzzle detail returns to the ranking platform tab', async () =>
expect(await screen.findByTestId('puzzle-board')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '返回上一页' }));
await user.click(
within(
await screen.findByRole('dialog', {
name: /\s*!/u,
}),
).getByRole('button', { name: '保存并退出' }),
);
await waitFor(() => {
expect(screen.getByRole('button', { name: '启动' })).toBeTruthy();
@@ -2983,6 +3080,98 @@ test('formal puzzle next level uses backend run and leaderboard keeps frontend l
expect(screen.getByText('测试玩家')).toBeTruthy();
});
test('first puzzle runtime back click can open remix result page', async () => {
const user = userEvent.setup();
const puzzleWork: PuzzleWorkSummary = {
workId: 'puzzle-work-public-1',
profileId: 'puzzle-profile-public-1',
ownerUserId: 'user-2',
sourceSessionId: null,
authorDisplayName: '拼图作者',
levelName: '雨夜猫塔',
summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
themeTags: ['雨夜', '猫咪', '遗迹'],
coverImageSrc: null,
coverAssetId: null,
publicationStatus: 'published',
updatedAt: '2026-04-25T12:10:00.000Z',
publishedAt: '2026-04-25T12:10:00.000Z',
playCount: 8,
remixCount: 0,
likeCount: 0,
publishReady: true,
};
const anchorPack = buildPuzzleAnchorPack();
const remixDraft: PuzzleResultDraft = {
workTitle: '改造后的雨夜猫塔',
workDescription: '准备改造的拼图草稿。',
levelName: '改造后的雨夜猫塔',
summary: '一只猫站在雨夜塔顶。',
themeTags: ['雨夜', '猫咪', '塔'],
forbiddenDirectives: [],
creatorIntent: null,
anchorPack,
candidates: [],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'idle',
levels: [],
metadata: null,
};
const remixSession: PuzzleAgentSessionSnapshot = {
sessionId: 'puzzle-session-remix-1',
currentTurn: 1,
progressPercent: 100,
stage: 'ready_to_publish',
anchorPack,
draft: remixDraft,
messages: [],
lastAssistantReply: null,
publishedProfileId: null,
suggestedActions: [],
resultPreview: null,
updatedAt: '2026-04-25T12:12:00.000Z',
};
vi.mocked(listPuzzleGallery).mockResolvedValue({
items: [puzzleWork],
});
vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
item: puzzleWork,
});
vi.mocked(startPuzzleRun).mockResolvedValue({
run: buildMockPuzzleRun(puzzleWork.profileId, puzzleWork.levelName),
});
vi.mocked(remixPuzzleGalleryWork).mockResolvedValue({
session: remixSession,
});
render(<TestWrapper withAuth />);
const searchInput = await screen.findByPlaceholderText(
'搜索作品号、名称、作者、描述',
);
await user.type(searchInput, 'PZ-EPUBLIC1');
await user.click(screen.getByRole('button', { name: '搜索' }));
await user.click(await screen.findByRole('button', { name: '启动' }));
expect(await screen.findByTestId('puzzle-board')).toBeTruthy();
await user.click(await screen.findByRole('button', { name: '返回上一页' }));
const dialog = await screen.findByRole('dialog', {
name: /\s*!/u,
});
await user.click(within(dialog).getByRole('button', { name: '作品改造' }));
await waitFor(() => {
expect(remixPuzzleGalleryWork).toHaveBeenCalledWith(
'puzzle-profile-public-1',
);
});
expect(await screen.findByText('拼图结果页')).toBeTruthy();
expect(screen.getByDisplayValue('改造后的雨夜猫塔')).toBeTruthy();
});
test('public code search opens a published puzzle by PZ code', async () => {
const user = userEvent.setup();
const puzzleWork: PuzzleWorkSummary = {