import assert from 'node:assert/strict'; import test from 'node:test'; import type { CustomWorldAgentActionExecutorMap } from './customWorldAgentActionExecutors/index.js'; import { CustomWorldAgentActionRegistry } from './customWorldAgentActionRegistry.js'; import { createRpgAgentSessionFixture } from '../../../packages/shared/src/contracts/rpgCreationFixtures.js'; function createExecutorLog() { const calls: Array<{ action: keyof CustomWorldAgentActionExecutorMap; payload: unknown; userId: string; sessionId: string; operationId: string; }> = []; const createExecutor = ( action: K, ): CustomWorldAgentActionExecutorMap[K] => { return (async (params) => { calls.push({ action, payload: params.payload, userId: params.userId, sessionId: params.sessionId, operationId: params.operationId, }); }) as CustomWorldAgentActionExecutorMap[K]; }; return { calls, executors: { draft_foundation: createExecutor('draft_foundation'), update_draft_card: createExecutor('update_draft_card'), sync_result_profile: createExecutor('sync_result_profile'), generate_characters: createExecutor('generate_characters'), generate_landmarks: createExecutor('generate_landmarks'), generate_role_assets: createExecutor('generate_role_assets'), sync_role_assets: createExecutor('sync_role_assets'), generate_scene_assets: createExecutor('generate_scene_assets'), sync_scene_assets: createExecutor('sync_scene_assets'), expand_long_tail: createExecutor('expand_long_tail'), publish_world: createExecutor('publish_world'), revert_checkpoint: createExecutor('revert_checkpoint'), } satisfies CustomWorldAgentActionExecutorMap, }; } function createSessionRecord(overrides: Partial> = {}) { const session = createRpgAgentSessionFixture(); return { ...JSON.parse(JSON.stringify(session)), userId: 'fixture-user', seedText: '被海雾吞没的旧航路群岛', operations: [], checkpoints: [], createdAt: session.updatedAt, updatedAt: session.updatedAt, ...overrides, }; } test('action registry exposes supported actions with stage-aware enablement and disabled reasons', () => { const { executors } = createExecutorLog(); const registry = new CustomWorldAgentActionRegistry(executors); const session = createSessionRecord({ stage: 'foundation_review', progressPercent: 80, }); const supportedActions = registry.buildSupportedActions(session as never); const draftFoundation = supportedActions.find( (entry) => entry.action === 'draft_foundation', ); const syncResultProfile = supportedActions.find( (entry) => entry.action === 'sync_result_profile', ); const publishWorld = supportedActions.find( (entry) => entry.action === 'publish_world', ); const expandLongTail = supportedActions.find( (entry) => entry.action === 'expand_long_tail', ); const revertCheckpoint = supportedActions.find( (entry) => entry.action === 'revert_checkpoint', ); assert.equal(draftFoundation?.enabled, false); assert.match(draftFoundation?.reason ?? '', /progressPercent >= 100/u); assert.equal(syncResultProfile?.enabled, false); assert.match( syncResultProfile?.reason ?? '', /object_refining or visual_refining/u, ); assert.equal(publishWorld?.enabled, false); assert.match( publishWorld?.reason ?? '', /object_refining, visual_refining, long_tail_review or ready_to_publish/u, ); assert.equal(expandLongTail?.enabled, false); assert.match( expandLongTail?.reason ?? '', /object_refining, visual_refining, long_tail_review or ready_to_publish/u, ); assert.equal(revertCheckpoint?.enabled, false); assert.match( revertCheckpoint?.reason ?? '', /requires at least one restorable checkpoint snapshot/u, ); }); test('action registry enables long-tail and publish actions in late stages, and exposes revert when restorable checkpoint exists', () => { const { executors } = createExecutorLog(); const registry = new CustomWorldAgentActionRegistry(executors); const session = createSessionRecord({ stage: 'ready_to_publish', checkpoints: [ { checkpointId: 'checkpoint-1', createdAt: '2026-04-21T12:00:00.000Z', label: '可回滚版本', snapshot: { currentTurn: 2, anchorContent: createSessionRecord().anchorContent, progressPercent: 100, lastAssistantReply: '已生成草稿。', stage: 'object_refining', focusCardId: 'world-foundation', creatorIntent: {}, creatorIntentReadiness: { isReady: true, completedKeys: [], missingKeys: [], }, anchorPack: {}, lockState: {}, draftProfile: createSessionRecord().draftProfile, pendingClarifications: [], suggestedActions: [], recommendedReplies: [], draftCards: createSessionRecord().draftCards, qualityFindings: [], assetCoverage: createSessionRecord().assetCoverage, }, }, ], }); const supportedActions = registry.buildSupportedActions(session as never); assert.equal( supportedActions.find((entry) => entry.action === 'expand_long_tail')?.enabled, true, ); assert.equal( supportedActions.find((entry) => entry.action === 'publish_world')?.enabled, true, ); assert.equal( supportedActions.find((entry) => entry.action === 'revert_checkpoint')?.enabled, true, ); }); test('action registry validates sync_scene_assets required payload and dispatches scene action executors', async () => { const { calls, executors } = createExecutorLog(); const registry = new CustomWorldAgentActionRegistry(executors); const session = createSessionRecord({ stage: 'visual_refining', }); assert.throws( () => registry.prepareExecution(session as never, { action: 'sync_scene_assets', sceneId: 'camp-home', sceneKind: 'camp', imageSrc: '', generatedSceneAssetId: 'scene-asset-1', }), /imageSrc and generatedSceneAssetId/u, ); const prepared = registry.prepareExecution(session as never, { action: 'generate_scene_assets', sceneIds: ['camp-home'], }); assert.equal(prepared.operationType, 'generate_scene_assets'); await prepared.execute({ userId: 'fixture-user', sessionId: 'fixture-session', operationId: 'operation-scene-1', }); assert.equal(calls.at(-1)?.action, 'generate_scene_assets'); }); test('action registry normalizes sync_result_profile payload before dispatching executor', async () => { const { calls, executors } = createExecutorLog(); const registry = new CustomWorldAgentActionRegistry(executors); const session = createSessionRecord({ stage: 'object_refining', }); const prepared = registry.prepareExecution(session as never, { action: 'sync_result_profile', profile: { id: 'profile-1', settingText: '潮雾列岛', name: '潮雾列岛', subtitle: '旧灯塔与失控航路', summary: '结果页确认版。', tone: '压抑、潮湿、悬疑', playerGoal: '查清真相。', templateWorldType: 'WUXIA', majorFactions: ['守灯会'], coreConflicts: ['争夺旧航路控制权'], playableNpcs: [], storyNpcs: [], items: [], landmarks: [], generationMode: 'full', generationStatus: 'complete', }, }); assert.equal(prepared.operationType, 'sync_result_profile'); await prepared.execute({ userId: 'fixture-user', sessionId: 'fixture-session', operationId: 'operation-1', }); assert.equal(calls.length, 1); assert.equal(calls[0]?.action, 'sync_result_profile'); assert.equal( (calls[0]?.payload as { profile?: { name?: string } })?.profile?.name, '潮雾列岛', ); }); test('action registry rejects invalid generate_role_assets payload with unit-level validation', () => { const { executors } = createExecutorLog(); const registry = new CustomWorldAgentActionRegistry(executors); const session = createSessionRecord({ stage: 'object_refining', }); assert.throws( () => registry.prepareExecution(session as never, { action: 'generate_role_assets', roleIds: ['playable-1', 'story-1'], }), /exactly one roleId/u, ); });