1
This commit is contained in:
260
server-node/src/services/customWorldAgentActionRegistry.test.ts
Normal file
260
server-node/src/services/customWorldAgentActionRegistry.test.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user