261 lines
8.4 KiB
TypeScript
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,
|
|
);
|
|
});
|