import { describe, expect, test } from 'vitest'; import type { BarkBattleDraftConfig } from '../../../packages/shared/src/contracts/barkBattle'; import type { BigFishSessionSnapshotResponse } from '../../../packages/shared/src/contracts/bigFish'; import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject'; import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent'; import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; import type { SquareHoleSessionSnapshot } from '../../../packages/shared/src/contracts/squareHoleAgent'; import type { VisualNovelAgentSessionSnapshot } from '../../../packages/shared/src/contracts/visualNovel'; import type { JumpHopSessionSnapshotResponse, JumpHopWorkProfileResponse, } from '../../services/jump-hop/jumpHopClient'; import type { WoodenFishSessionSnapshotResponse, WoodenFishWorkProfileResponse, } from '../../services/wooden-fish/woodenFishClient'; import { buildBabyObjectMatchCreationUrlState, buildBarkBattleCreationUrlState, buildBigFishCreationUrlState, buildJumpHopCreationUrlState, buildMatch3DCreationUrlState, buildPuzzleCreationUrlState, buildPuzzleDraftRuntimeUrlState, buildPuzzlePublishedRuntimeUrlState, buildPuzzleRuntimeUrlStateKey, buildSquareHoleCreationUrlState, buildVisualNovelCreationUrlState, buildWoodenFishCreationUrlState, hasCreationUrlStateValue, hasPuzzleRuntimeUrlStateValue, matchesBabyObjectMatchCreationUrlRestoreTarget, matchesBarkBattleCreationUrlRestoreTarget, matchesBigFishCreationUrlRestoreTarget, matchesSessionProfileWorkCreationUrlRestoreTarget, matchesVisualNovelCreationUrlRestoreTarget, normalizeCreationUrlValue, resolveCreationUrlRestoreTarget, resolveInitialCreationUrlRestoreDecision, resolveJumpHopCreationUrlRestoreStage, resolveWoodenFishCreationUrlRestoreStage, } from './platformCreationUrlStateModel'; describe('platformCreationUrlStateModel', () => { test('normalizes private creation url state values', () => { expect(normalizeCreationUrlValue(' session-1 ')).toBe('session-1'); expect(normalizeCreationUrlValue(' ')).toBeNull(); expect( hasCreationUrlStateValue({ sessionId: ' ', profileId: null, draftId: undefined, workId: 'work-1', }), ).toBe(true); expect(hasCreationUrlStateValue({})).toBe(false); }); test('resolves initial creation url restore readiness', () => { const readyParams = { handled: false, pathname: '/creation/puzzle/result', state: { sessionId: 'puzzle-session-1' }, isLoadingPlatform: false, canReadProtectedData: true, }; expect( resolveInitialCreationUrlRestoreDecision({ ...readyParams, handled: true, }), ).toEqual({ type: 'skip' }); expect( resolveInitialCreationUrlRestoreDecision({ ...readyParams, pathname: '/works/detail', }), ).toEqual({ type: 'mark-handled' }); expect( resolveInitialCreationUrlRestoreDecision({ ...readyParams, state: {}, }), ).toEqual({ type: 'mark-handled' }); expect( resolveInitialCreationUrlRestoreDecision({ ...readyParams, isLoadingPlatform: true, }), ).toEqual({ type: 'wait' }); expect( resolveInitialCreationUrlRestoreDecision({ ...readyParams, canReadProtectedData: false, }), ).toEqual({ type: 'wait' }); expect(resolveInitialCreationUrlRestoreDecision(readyParams)).toEqual({ type: 'restore', }); }); test('resolves supported creation url restore targets from paths', () => { const state = { sessionId: ' session-1 ', profileId: ' profile-1 ', draftId: ' draft-1 ', workId: ' work-1 ', }; const cases = [ ['/creation/big-fish/result', 'big-fish'], ['/creation/match3d/result', 'match3d'], ['/creation/square-hole/result', 'square-hole'], ['/creation/puzzle/result', 'puzzle'], ['/creation/visual-novel/result', 'visual-novel'], ['/creation/bark-battle/result', 'bark-battle'], ['/creation/baby-object-match/result', 'baby-object-match'], ['/creation/jump-hop/result', 'jump-hop'], ['/creation/wooden-fish/result', 'wooden-fish'], ] as const; cases.forEach(([pathname, kind]) => { expect(resolveCreationUrlRestoreTarget(pathname, state)).toMatchObject({ kind, sessionId: 'session-1', profileId: 'profile-1', draftId: 'draft-1', workId: 'work-1', isGeneratingPath: false, }); }); }); test('normalizes creation url restore target values and generating paths', () => { expect( resolveCreationUrlRestoreTarget('/creation/jump-hop/generating', { sessionId: ' ', profileId: ' jump-profile-1 ', draftId: undefined, workId: null, }), ).toEqual({ kind: 'jump-hop', sessionId: null, profileId: 'jump-profile-1', draftId: null, workId: null, isGeneratingPath: true, }); }); test('derives big fish restore session from work id when needed', () => { expect( resolveCreationUrlRestoreTarget('/creation/big-fish/result', { workId: 'big-fish-work-river', }), ).toEqual({ kind: 'big-fish', sessionId: null, profileId: null, draftId: null, workId: 'big-fish-work-river', isGeneratingPath: false, bigFishSessionId: 'river', }); expect( resolveCreationUrlRestoreTarget('/creation/big-fish/result', { sessionId: 'big-fish-session-carp', workId: 'big-fish-work-river', }), ).toMatchObject({ kind: 'big-fish', bigFishSessionId: 'big-fish-session-carp', }); }); test('keeps unsupported creation paths without a concrete restore target', () => { expect( resolveCreationUrlRestoreTarget('/creation/rpg/result', { sessionId: 'rpg-session-1', }), ).toBeNull(); expect( resolveCreationUrlRestoreTarget('/creation/unknown/result', { sessionId: 'unknown-session-1', }), ).toBeNull(); expect( resolveCreationUrlRestoreTarget('/creation/big-fishery/result', { sessionId: 'big-fish-session-1', }), ).toBeNull(); expect( resolveCreationUrlRestoreTarget('/works/detail', { workId: 'work-1', }), ).toBeNull(); }); test('matches restore targets against work and draft identities', () => { const bigFishTarget = resolveCreationUrlRestoreTarget( '/creation/big-fish/result', { workId: 'big-fish-work-river', }, ); expect(bigFishTarget?.kind).toBe('big-fish'); if (bigFishTarget?.kind !== 'big-fish') { throw new Error('big fish target expected'); } expect( matchesBigFishCreationUrlRestoreTarget( { sourceSessionId: 'river' }, bigFishTarget, ), ).toBe(true); expect( matchesBigFishCreationUrlRestoreTarget( { workId: 'big-fish-work-river' }, bigFishTarget, ), ).toBe(true); const target = { sessionId: 'session-1', profileId: 'profile-1', draftId: 'draft-1', workId: 'work-1', }; expect( matchesSessionProfileWorkCreationUrlRestoreTarget( { sourceSessionId: 'session-1' }, target, ), ).toBe(true); expect( matchesSessionProfileWorkCreationUrlRestoreTarget( { profileId: 'profile-1' }, target, ), ).toBe(true); expect( matchesSessionProfileWorkCreationUrlRestoreTarget( { workId: 'work-1' }, target, ), ).toBe(true); expect( matchesVisualNovelCreationUrlRestoreTarget( { profileId: 'profile-1' }, target, ), ).toBe(true); expect( matchesBarkBattleCreationUrlRestoreTarget( { draftId: 'draft-1' }, target, ), ).toBe(true); expect( matchesBabyObjectMatchCreationUrlRestoreTarget( { profileId: 'work-1' }, target, ), ).toBe(true); expect( matchesSessionProfileWorkCreationUrlRestoreTarget( { sourceSessionId: null, profileId: null, workId: null }, { sessionId: null, profileId: null, workId: null }, ), ).toBe(false); expect( matchesBarkBattleCreationUrlRestoreTarget( { workId: null, draftId: null }, { workId: null, draftId: null }, ), ).toBe(false); }); test('resolves work backed restore stages', () => { expect( resolveJumpHopCreationUrlRestoreStage({ isGeneratingPath: true, hasRestoredDraft: false, hasRestoredWork: true, }), ).toBe('jump-hop-generating'); expect( resolveJumpHopCreationUrlRestoreStage({ isGeneratingPath: false, hasRestoredDraft: false, hasRestoredWork: true, }), ).toBe('jump-hop-result'); expect( resolveJumpHopCreationUrlRestoreStage({ isGeneratingPath: false, hasRestoredDraft: false, hasRestoredWork: false, }), ).toBe('jump-hop-workspace'); expect( resolveWoodenFishCreationUrlRestoreStage({ isGeneratingPath: true, hasRestoredDraft: true, }), ).toBe('wooden-fish-generating'); expect( resolveWoodenFishCreationUrlRestoreStage({ isGeneratingPath: false, hasRestoredDraft: true, }), ).toBe('wooden-fish-result'); expect( resolveWoodenFishCreationUrlRestoreStage({ isGeneratingPath: false, hasRestoredDraft: false, }), ).toBe('wooden-fish-workspace'); }); test('builds creation restore state for core session based plays', () => { expect( buildBigFishCreationUrlState({ sessionId: ' big-fish-session-1 ', } as BigFishSessionSnapshotResponse), ).toEqual({ sessionId: 'big-fish-session-1', workId: 'big-fish-work-big-fish-session-1', }); expect( buildMatch3DCreationUrlState({ sessionId: 'match3d-session-1', draft: { profileId: 'match3d-profile-draft' }, } as Match3DAgentSessionSnapshot), ).toEqual({ sessionId: 'match3d-session-1', profileId: 'match3d-profile-draft', workId: 'match3d-profile-draft', }); expect( buildSquareHoleCreationUrlState({ sessionId: 'square-session-1', publishedProfileId: 'square-profile-published', } as SquareHoleSessionSnapshot), ).toEqual({ sessionId: 'square-session-1', profileId: 'square-profile-published', workId: 'square-profile-published', }); expect( buildVisualNovelCreationUrlState({ sessionId: 'visual-session-1', draft: { profileId: 'visual-profile-1' }, } as VisualNovelAgentSessionSnapshot), ).toEqual({ sessionId: 'visual-session-1', profileId: 'visual-profile-1', workId: 'visual-profile-1', }); }); test('builds puzzle creation and runtime query state', () => { expect( buildPuzzleCreationUrlState({ sessionId: 'puzzle-session-ocean', } as PuzzleAgentSessionSnapshot), ).toEqual({ sessionId: 'puzzle-session-ocean', profileId: 'puzzle-profile-ocean', workId: 'puzzle-work-ocean', }); const draftRuntime = buildPuzzleDraftRuntimeUrlState( buildPuzzleWork({ profileId: 'puzzle-profile-ocean', sourceSessionId: null, }), 'level-2', ); expect(draftRuntime).toEqual({ mode: 'draft', runtimeSessionId: 'puzzle-session-ocean', runtimeProfileId: 'puzzle-profile-ocean', runtimeLevelId: 'level-2', }); expect(hasPuzzleRuntimeUrlStateValue(draftRuntime)).toBe(true); expect(buildPuzzleRuntimeUrlStateKey(draftRuntime)).toBe( 'draft|puzzle-session-ocean|puzzle-profile-ocean|level-2|', ); const publishedRuntime = buildPuzzlePublishedRuntimeUrlState( buildPuzzleWork({ profileId: 'puzzle-profile-ocean' }), ); expect(publishedRuntime.mode).toBe('published'); expect(publishedRuntime.runtimeProfileId).toBe('puzzle-profile-ocean'); expect(publishedRuntime.publicWorkCode).toMatch(/^PZ-/u); }); test('builds creation state for work backed plays with work id priority', () => { expect( buildJumpHopCreationUrlState({ session: { sessionId: 'jump-session-1', draft: { profileId: 'jump-profile-draft' }, } as JumpHopSessionSnapshotResponse, work: { summary: { profileId: 'jump-profile-work', workId: 'jump-work-1', }, } as JumpHopWorkProfileResponse, }), ).toEqual({ sessionId: 'jump-session-1', profileId: 'jump-profile-work', workId: 'jump-work-1', }); expect( buildWoodenFishCreationUrlState({ session: { sessionId: 'wood-session-1', draft: { profileId: 'wood-profile-draft' }, } as WoodenFishSessionSnapshotResponse, work: { summary: { profileId: 'wood-profile-work', workId: 'wood-work-1', }, } as WoodenFishWorkProfileResponse, }), ).toEqual({ sessionId: 'wood-session-1', profileId: 'wood-profile-work', draftId: 'wood-profile-work', workId: 'wood-work-1', }); }); test('builds creation state for draft backed local plays', () => { expect( buildBarkBattleCreationUrlState({ draftId: 'bark-draft-1', workId: 'bark-work-1', } as BarkBattleDraftConfig), ).toEqual({ draftId: 'bark-draft-1', workId: 'bark-work-1', }); expect( buildBabyObjectMatchCreationUrlState({ draftId: 'baby-draft-1', profileId: 'baby-profile-1', } as BabyObjectMatchDraft), ).toEqual({ profileId: 'baby-profile-1', draftId: 'baby-draft-1', workId: 'baby-profile-1', }); }); }); function buildPuzzleWork( overrides: Partial = {}, ): PuzzleWorkSummary { return { workId: 'puzzle-work-base', profileId: 'puzzle-profile-base', ownerUserId: 'user-1', sourceSessionId: 'puzzle-session-base', authorDisplayName: '测试作者', workTitle: '潮雾拼图', workDescription: '潮雾港口拼图。', levelName: '潮雾拼图', summary: '潮雾港口拼图。', themeTags: [], coverImageSrc: null, coverAssetId: null, publicationStatus: 'draft', updatedAt: '2026-06-03T08:00:00.000Z', publishedAt: null, playCount: 0, remixCount: 0, likeCount: 0, publishReady: false, levels: [], ...overrides, }; }