Merge branch 'master' into codex/ddd
This commit is contained in:
@@ -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';
|
||||
@@ -621,6 +628,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;
|
||||
@@ -1810,7 +1852,7 @@ beforeEach(() => {
|
||||
vi.mocked(streamRpgCreationMessage).mockResolvedValue(mockSession);
|
||||
});
|
||||
|
||||
test('create hub keeps RPG, AIRP and visual novel locked', async () => {
|
||||
test('create hub opens RPG while keeping AIRP and visual novel locked', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
@@ -1826,9 +1868,13 @@ test('create hub keeps RPG, AIRP and visual novel locked', async () => {
|
||||
expect((visualNovelButton as HTMLButtonElement).disabled).toBe(true);
|
||||
const rpgButton = screen.getByRole('button', { name: /角色扮演/u });
|
||||
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(within(rpgButton).getAllByText('敬请期待').length).toBeGreaterThan(0);
|
||||
expect(createRpgCreationSession).not.toHaveBeenCalled();
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(false);
|
||||
await user.click(rpgButton);
|
||||
|
||||
expect(createRpgCreationSession).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
await screen.findByText('Agent工作区:custom-world-agent-session-1'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('platform create hub does not prefetch hidden big fish platform data', async () => {
|
||||
@@ -2225,6 +2271,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();
|
||||
@@ -2523,6 +2617,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();
|
||||
@@ -2539,7 +2640,7 @@ test('published puzzle detail returns to the ranking platform tab', async () =>
|
||||
});
|
||||
});
|
||||
|
||||
test('selecting locked RPG creation while logged out does not route through requireAuth', async () => {
|
||||
test('selecting RPG creation while logged out routes through requireAuth', async () => {
|
||||
const user = userEvent.setup();
|
||||
const requireAuth = vi.fn();
|
||||
|
||||
@@ -2556,9 +2657,9 @@ test('selecting locked RPG creation while logged out does not route through requ
|
||||
await openCreationHub(user);
|
||||
const rpgButton = await screen.findByRole('button', { name: /角色扮演/u });
|
||||
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(true);
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(false);
|
||||
await user.click(rpgButton);
|
||||
expect(requireAuth).not.toHaveBeenCalled();
|
||||
expect(requireAuth).toHaveBeenCalledTimes(1);
|
||||
expect(createRpgCreationSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -2684,16 +2785,16 @@ test('new creation entry maps raw bearer token errors to user-facing auth copy',
|
||||
await openCreationHub(user);
|
||||
const rpgButton = screen.getByRole('button', { name: /角色扮演/u });
|
||||
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(true);
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(false);
|
||||
await user.click(rpgButton);
|
||||
|
||||
expect(listPuzzleWorks).toHaveBeenCalled();
|
||||
expect(createRpgCreationSession).not.toHaveBeenCalled();
|
||||
expect(createRpgCreationSession).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
within(getPlatformTabPanel('create')).queryByText(
|
||||
await within(getPlatformTabPanel('create')).findByText(
|
||||
'当前登录状态已失效,请重新登录后继续。',
|
||||
),
|
||||
).toBeNull();
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText('缺少 Authorization Bearer Token')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -3016,6 +3117,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 = {
|
||||
|
||||
@@ -29,6 +29,7 @@ export type PlatformPuzzleGalleryCard = {
|
||||
sourceType: 'puzzle';
|
||||
workId: string;
|
||||
profileId: string;
|
||||
sourceSessionId?: string | null;
|
||||
publicWorkCode: string;
|
||||
ownerUserId: string;
|
||||
authorDisplayName: string;
|
||||
@@ -78,6 +79,7 @@ export type PlatformMatch3DGalleryCard = {
|
||||
sourceType: 'match3d';
|
||||
workId: string;
|
||||
profileId: string;
|
||||
sourceSessionId?: string | null;
|
||||
publicWorkCode: string;
|
||||
ownerUserId: string;
|
||||
authorDisplayName: string;
|
||||
@@ -132,6 +134,7 @@ export function mapPuzzleWorkToPlatformGalleryCard(
|
||||
sourceType: 'puzzle',
|
||||
workId: work.workId,
|
||||
profileId: work.profileId,
|
||||
sourceSessionId: work.sourceSessionId ?? null,
|
||||
publicWorkCode: buildPuzzlePublicWorkCode(work.profileId),
|
||||
ownerUserId: work.ownerUserId,
|
||||
authorDisplayName: work.authorDisplayName,
|
||||
@@ -158,6 +161,7 @@ export function mapMatch3DWorkToPlatformGalleryCard(
|
||||
sourceType: 'match3d',
|
||||
workId: work.workId,
|
||||
profileId: work.profileId,
|
||||
sourceSessionId: work.sourceSessionId ?? null,
|
||||
publicWorkCode: buildMatch3DPublicWorkCode(work.profileId),
|
||||
ownerUserId: work.ownerUserId,
|
||||
authorDisplayName: '玩家',
|
||||
|
||||
Reference in New Issue
Block a user