Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-01 16:26:19 +08:00
30 changed files with 2104 additions and 436 deletions

View File

@@ -10,6 +10,8 @@ import type {
CustomWorldAgentSessionSnapshot,
CustomWorldWorkSummary,
} from '../../../packages/shared/src/contracts/customWorldAgent';
import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
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';
@@ -30,6 +32,20 @@ import {
startLocalBigFishRuntimeRun,
} from '../../services/big-fish-runtime';
import { listBigFishWorks } from '../../services/big-fish-works';
import { match3dCreationClient } from '../../services/match3d-creation';
import {
clickMatch3DItem,
finishMatch3DTimeUp,
restartMatch3DRun,
startMatch3DRun,
stopMatch3DRun,
} from '../../services/match3d-runtime';
import {
deleteMatch3DWork,
getMatch3DWorkDetail,
listMatch3DGallery,
listMatch3DWorks,
} from '../../services/match3d-works';
import {
createPuzzleAgentSession,
getPuzzleAgentSession,
@@ -242,10 +258,34 @@ vi.mock('../../services/big-fish-gallery', () => ({
vi.mock('../../services/big-fish-runtime', () => ({
advanceLocalBigFishRuntimeRun: vi.fn((run) => run),
recordBigFishPlay: vi.fn(),
recordBigFishPlay: vi.fn(() => Promise.resolve()),
startLocalBigFishRuntimeRun: vi.fn(),
}));
vi.mock('../../services/match3d-creation', () => ({
match3dCreationClient: {
createSession: vi.fn(),
executeAction: vi.fn(),
getSession: vi.fn(),
streamMessage: vi.fn(),
},
}));
vi.mock('../../services/match3d-works', () => ({
deleteMatch3DWork: vi.fn(),
getMatch3DWorkDetail: vi.fn(),
listMatch3DGallery: vi.fn(),
listMatch3DWorks: vi.fn(),
}));
vi.mock('../../services/match3d-runtime', () => ({
clickMatch3DItem: vi.fn(),
finishMatch3DTimeUp: vi.fn(),
restartMatch3DRun: vi.fn(),
startMatch3DRun: vi.fn(),
stopMatch3DRun: vi.fn(),
}));
vi.mock('../../services/puzzle-runtime/puzzleLocalRuntime', async () => {
const actual = await vi.importActual<
typeof import('../../services/puzzle-runtime/puzzleLocalRuntime')
@@ -399,6 +439,23 @@ vi.mock('../big-fish-result/BigFishResultView', () => ({
),
}));
vi.mock('../match3d-runtime/Match3DRuntimeShell', () => ({
Match3DRuntimeShell: ({
run,
onBack,
}: {
run: Match3DRunSnapshot | null;
onBack: () => void;
}) => (
<div className="match3d-runtime-shell-mock">
<div>{run?.runId ?? 'missing-run'}</div>
<button type="button" onClick={onBack}>
</button>
</div>
),
}));
vi.mock('../custom-world-agent/CustomWorldAgentWorkspace', () => ({
CustomWorldAgentWorkspace: ({
session,
@@ -599,6 +656,26 @@ function buildClearedPuzzleRun(params: {
};
}
function buildMockMatch3DRun(profileId: string): Match3DRunSnapshot {
return {
runId: `match3d-run-${profileId}`,
profileId,
ownerUserId: 'user-2',
status: 'running',
snapshotVersion: 1,
startedAtMs: 1_000,
durationLimitMs: 600_000,
serverNowMs: 1_000,
remainingMs: 600_000,
clearCount: 4,
totalItemCount: 12,
clearedItemCount: 0,
items: [],
traySlots: Array.from({ length: 7 }, (_, slotIndex) => ({ slotIndex })),
failureReason: null,
};
}
function buildMockRpgGalleryDetail(
entry: CustomWorldGalleryCard,
): CustomWorldLibraryEntry<CustomWorldProfile> {
@@ -1056,6 +1133,7 @@ beforeEach(() => {
vi.mocked(upsertProfileBrowseHistory).mockResolvedValue([]);
vi.mocked(clearProfileBrowseHistory).mockResolvedValue([]);
vi.mocked(deleteRpgEntryWorldProfile).mockResolvedValue([]);
vi.mocked(recordBigFishPlay).mockResolvedValue(undefined);
vi.mocked(recordRpgEntryWorldGalleryPlay).mockImplementation(
async (ownerUserId, profileId) => ({
ownerUserId,
@@ -1500,8 +1578,43 @@ beforeEach(() => {
vi.mocked(listBigFishGallery).mockResolvedValue({
items: [],
});
vi.mocked(recordBigFishPlay).mockResolvedValue({
session: {} as never,
vi.mocked(recordBigFishPlay).mockResolvedValue(undefined);
vi.mocked(match3dCreationClient.createSession).mockResolvedValue({
session: null,
});
vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
session: null,
});
vi.mocked(match3dCreationClient.streamMessage).mockResolvedValue(null);
vi.mocked(match3dCreationClient.executeAction).mockResolvedValue({
session: null,
});
vi.mocked(listMatch3DWorks).mockResolvedValue({
items: [],
});
vi.mocked(listMatch3DGallery).mockResolvedValue({
items: [],
});
vi.mocked(getMatch3DWorkDetail).mockRejectedValue(
new Error('未找到抓大鹅作品'),
);
vi.mocked(deleteMatch3DWork).mockResolvedValue({
items: [],
});
vi.mocked(startMatch3DRun).mockRejectedValue(
new Error('未启动抓大鹅运行态'),
);
vi.mocked(clickMatch3DItem).mockRejectedValue(
new Error('未执行抓大鹅点击'),
);
vi.mocked(restartMatch3DRun).mockRejectedValue(
new Error('未重新开始抓大鹅运行态'),
);
vi.mocked(finishMatch3DTimeUp).mockResolvedValue({
run: buildMockMatch3DRun('match3d-profile-time-up'),
});
vi.mocked(stopMatch3DRun).mockResolvedValue({
run: buildMockMatch3DRun('match3d-profile-stopped'),
});
vi.mocked(startLocalBigFishRuntimeRun).mockReturnValue({
runId: 'big-fish-run-1',
@@ -2057,7 +2170,7 @@ test('logged out public detail gates big fish start before local runtime', async
);
const searchInput = await screen.findByPlaceholderText(
'输入 SY / CW / BF / PZ 编号',
'输入 SY / CW / BF / M3 / PZ 编号',
);
await user.type(searchInput, 'BF-NPUBLIC1');
await user.click(screen.getByRole('button', { name: '搜索' }));
@@ -2518,7 +2631,6 @@ test('puzzle creation timeout exits busy state and shows a readable error', asyn
const button = screen.getByRole('button', { name: /.*/u });
await user.click(button);
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
await waitFor(() => {
expect(
@@ -2527,9 +2639,7 @@ test('puzzle creation timeout exits busy state and shows a readable error', asyn
).length,
).toBeGreaterThan(0);
});
expect(
screen.getByRole('button', { name: '生成草稿' }) as HTMLButtonElement,
).toHaveProperty('disabled', false);
expect(button as HTMLButtonElement).toHaveProperty('disabled', false);
expect(screen.queryByText(//u)).toBeNull();
});
@@ -2734,7 +2844,7 @@ test('formal puzzle next level uses backend run and leaderboard keeps frontend l
render(<TestWrapper withAuth />);
const searchInput = await screen.findByPlaceholderText(
'输入 SY / CW / BF / PZ 编号',
'输入 SY / CW / BF / M3 / PZ 编号',
);
await user.type(searchInput, 'PZ-EPUBLIC1');
await user.click(screen.getByRole('button', { name: '搜索' }));
@@ -2807,7 +2917,7 @@ test('public code search opens a published puzzle by PZ code', async () => {
render(<TestWrapper withAuth />);
const searchInput = await screen.findByPlaceholderText(
'输入 SY / CW / BF / PZ 编号',
'输入 SY / CW / BF / M3 / PZ 编号',
);
await user.type(searchInput, 'PZ-EPUBLIC1');
await user.click(screen.getByRole('button', { name: '搜索' }));
@@ -2850,7 +2960,7 @@ test('public code search opens a published big fish work by BF code', async () =
render(<TestWrapper withAuth />);
const searchInput = await screen.findByPlaceholderText(
'输入 SY / CW / BF / PZ 编号',
'输入 SY / CW / BF / M3 / PZ 编号',
);
await user.type(searchInput, 'BF-NPUBLIC1');
await user.click(screen.getByRole('button', { name: '搜索' }));
@@ -2872,6 +2982,56 @@ test('public code search opens a published big fish work by BF code', async () =
expect(getRpgEntryWorldGalleryDetailByCode).not.toHaveBeenCalled();
});
test('public code search opens a published Match3D work by M3 code and starts runtime', async () => {
const user = userEvent.setup();
const match3dWork: Match3DWorkSummary = {
workId: 'match3d-work-public-1',
profileId: 'match3d-profile-public-1',
ownerUserId: 'user-2',
sourceSessionId: 'match3d-session-public-1',
gameName: '水果抓大鹅',
themeText: '水果消除',
summary: '把圆形空间里的水果全部消除。',
tags: ['水果', '消除'],
coverImageSrc: null,
referenceImageSrc: null,
clearCount: 4,
difficulty: 5,
publicationStatus: 'published',
playCount: 3,
updatedAt: '2026-04-25T10:30:00.000Z',
publishedAt: '2026-04-25T10:30:00.000Z',
publishReady: true,
};
vi.mocked(listMatch3DGallery).mockResolvedValue({
items: [match3dWork],
});
vi.mocked(startMatch3DRun).mockResolvedValue({
run: buildMockMatch3DRun(match3dWork.profileId),
});
render(<TestWrapper withAuth />);
const searchInput = await screen.findByPlaceholderText(
'输入 SY / CW / BF / M3 / PZ 编号',
);
await user.type(searchInput, 'M3-EPUBLIC1');
await user.click(screen.getByRole('button', { name: '搜索' }));
expect(await screen.findByText('详情')).toBeTruthy();
expect(screen.getByText('水果抓大鹅')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '启动' }));
await waitFor(() => {
expect(startMatch3DRun).toHaveBeenCalledWith('match3d-profile-public-1');
});
expect(
await screen.findByText('抓大鹅运行态match3d-run-match3d-profile-public-1'),
).toBeTruthy();
expect(getRpgEntryWorldGalleryDetailByCode).not.toHaveBeenCalled();
});
test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
const user = userEvent.setup();