Files
Genarrative/server-node/src/services/customWorldAgentActionRegistry.test.ts
2026-04-21 18:27:46 +08:00

261 lines
8.4 KiB
TypeScript

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 = <K extends keyof CustomWorldAgentActionExecutorMap>(
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<Record<string, unknown>> = {}) {
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,
);
});