@@ -1,8 +1,14 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import type { CustomWorldSessionRecord } from '../../../packages/shared/src/contracts/runtime.js';
|
||||
import type { AppConfig } from '../config.js';
|
||||
import type { RuntimeRepositoryPort } from '../repositories/runtimeRepository.js';
|
||||
import { CustomWorldAgentAutoAssetService } from './customWorldAgentAutoAssetService.js';
|
||||
import { normalizeFoundationDraftProfile } from './customWorldAgentDraftCompiler.js';
|
||||
import { CustomWorldAgentOrchestrator } from './customWorldAgentOrchestrator.js';
|
||||
import { CustomWorldAgentSessionStore } from './customWorldAgentSessionStore.js';
|
||||
import { createTestCustomWorldAgentSingleTurnLlmClient } from './customWorldAgentTestHelpers.js';
|
||||
@@ -88,6 +94,102 @@ function createRuntimeRepositoryStub(): RuntimeRepositoryPort {
|
||||
};
|
||||
}
|
||||
|
||||
function createAutoAssetTestConfig(testName: string): AppConfig {
|
||||
const projectRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), `genarrative-agent-phase3-${testName}-`),
|
||||
);
|
||||
|
||||
return {
|
||||
nodeEnv: 'test',
|
||||
projectRoot,
|
||||
publicDir: path.join(projectRoot, 'public'),
|
||||
logsDir: path.join(projectRoot, 'logs'),
|
||||
dataDir: path.join(projectRoot, 'data'),
|
||||
rawEnv: {},
|
||||
databaseUrl: `pg-mem://${testName}`,
|
||||
serverAddr: ':0',
|
||||
logLevel: 'silent',
|
||||
editorApiEnabled: true,
|
||||
assetsApiEnabled: true,
|
||||
jwtSecret: 'test',
|
||||
jwtExpiresIn: '7d',
|
||||
jwtIssuer: 'test',
|
||||
llm: {
|
||||
baseUrl: 'https://example.invalid',
|
||||
apiKey: '',
|
||||
model: 'test-model',
|
||||
},
|
||||
dashScope: {
|
||||
baseUrl: 'https://example.invalid',
|
||||
apiKey: '',
|
||||
imageModel: 'test-image-model',
|
||||
requestTimeoutMs: 1000,
|
||||
},
|
||||
smsAuth: {
|
||||
enabled: false,
|
||||
provider: 'mock',
|
||||
endpoint: '',
|
||||
accessKeyId: '',
|
||||
accessKeySecret: '',
|
||||
signName: '',
|
||||
templateCode: '',
|
||||
templateParamKey: '',
|
||||
countryCode: '86',
|
||||
schemeName: '',
|
||||
codeLength: 6,
|
||||
codeType: 1,
|
||||
validTimeSeconds: 300,
|
||||
intervalSeconds: 60,
|
||||
duplicatePolicy: 1,
|
||||
caseAuthPolicy: 1,
|
||||
returnVerifyCode: false,
|
||||
mockVerifyCode: '123456',
|
||||
maxSendPerPhonePerDay: 20,
|
||||
maxSendPerIpPerHour: 30,
|
||||
maxVerifyFailuresPerPhonePerHour: 12,
|
||||
maxVerifyFailuresPerIpPerHour: 24,
|
||||
captchaTtlSeconds: 180,
|
||||
captchaTriggerVerifyFailuresPerPhone: 3,
|
||||
captchaTriggerVerifyFailuresPerIp: 5,
|
||||
blockPhoneFailureThreshold: 6,
|
||||
blockIpFailureThreshold: 10,
|
||||
blockPhoneDurationMinutes: 30,
|
||||
blockIpDurationMinutes: 30,
|
||||
},
|
||||
wechatAuth: {
|
||||
enabled: false,
|
||||
provider: 'mock',
|
||||
appId: '',
|
||||
appSecret: '',
|
||||
authorizeEndpoint: '',
|
||||
accessTokenEndpoint: '',
|
||||
userInfoEndpoint: '',
|
||||
callbackPath: '',
|
||||
defaultRedirectPath: '/',
|
||||
mockUserId: '',
|
||||
mockUnionId: '',
|
||||
mockDisplayName: '',
|
||||
mockAvatarUrl: '',
|
||||
},
|
||||
authSession: {
|
||||
refreshCookieName: 'refresh_token',
|
||||
refreshSessionTtlDays: 30,
|
||||
refreshCookieSecure: false,
|
||||
refreshCookieSameSite: 'Lax',
|
||||
refreshCookiePath: '/',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createFallbackAutoAssetService(testName: string) {
|
||||
const config = createAutoAssetTestConfig(testName);
|
||||
return new CustomWorldAgentAutoAssetService(
|
||||
config,
|
||||
CustomWorldAgentAutoAssetService.createFallbackCharacterVisualGenerator(config),
|
||||
CustomWorldAgentAutoAssetService.createFallbackSceneActBackgroundGenerator(config),
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForOperation(
|
||||
orchestrator: CustomWorldAgentOrchestrator,
|
||||
userId: string,
|
||||
@@ -161,6 +263,7 @@ test('phase3 ready session can execute draft_foundation and expose card detail',
|
||||
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
|
||||
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
|
||||
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
|
||||
autoAssetService: createFallbackAutoAssetService('draft'),
|
||||
});
|
||||
const userId = 'user-phase3-draft';
|
||||
const readySession = await createReadySession(orchestrator, userId);
|
||||
@@ -179,6 +282,16 @@ test('phase3 ready session can execute draft_foundation and expose card detail',
|
||||
response.operation.operationId,
|
||||
);
|
||||
const snapshot = await orchestrator.getSessionSnapshot(userId, readySession.sessionId);
|
||||
const draftProfile = snapshot?.draftProfile as Record<string, unknown> | undefined;
|
||||
const playableNpcs = Array.isArray(draftProfile?.playableNpcs)
|
||||
? draftProfile?.playableNpcs
|
||||
: [];
|
||||
const storyNpcs = Array.isArray(draftProfile?.storyNpcs)
|
||||
? draftProfile?.storyNpcs
|
||||
: [];
|
||||
const sceneChapters = Array.isArray(draftProfile?.sceneChapters)
|
||||
? draftProfile?.sceneChapters
|
||||
: [];
|
||||
|
||||
assert.equal(operation?.status, 'completed');
|
||||
assert.equal(snapshot?.stage, 'object_refining');
|
||||
@@ -189,6 +302,23 @@ test('phase3 ready session can execute draft_foundation and expose card detail',
|
||||
assert.ok(snapshot?.draftCards.some((card) => card.kind === 'landmark'));
|
||||
assert.ok(snapshot?.draftCards.some((card) => card.kind === 'thread'));
|
||||
assert.ok(snapshot?.draftCards.some((card) => card.kind === 'chapter'));
|
||||
assert.equal(playableNpcs.length, 1);
|
||||
assert.ok(storyNpcs.length >= 4);
|
||||
assert.equal(sceneChapters.length, 2);
|
||||
assert.ok(
|
||||
sceneChapters.every(
|
||||
(entry) => Array.isArray((entry as { acts?: unknown[] }).acts) && ((entry as { acts?: unknown[] }).acts?.length ?? 0) === 3,
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
playableNpcs.every(
|
||||
(entry) =>
|
||||
typeof (entry as { imageSrc?: unknown }).imageSrc === 'string' &&
|
||||
typeof (entry as { generatedVisualAssetId?: unknown }).generatedVisualAssetId === 'string',
|
||||
),
|
||||
);
|
||||
assert.ok((snapshot?.assetCoverage.sceneAssets.length ?? 0) >= 6);
|
||||
assert.equal(snapshot?.assetCoverage.allSceneAssetsReady, true);
|
||||
assert.equal(
|
||||
typeof (snapshot?.draftProfile as Record<string, unknown>)?.name,
|
||||
'string',
|
||||
@@ -221,6 +351,7 @@ test('phase3 draft_foundation rejects not-ready session', async () => {
|
||||
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
|
||||
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
|
||||
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
|
||||
autoAssetService: createFallbackAutoAssetService('not-ready'),
|
||||
});
|
||||
const userId = 'user-phase3-not-ready';
|
||||
const createdSession = await orchestrator.createSession(userId, {
|
||||
@@ -241,6 +372,7 @@ test('phase3 work summaries prefer compiled foundation draft fields', async () =
|
||||
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
|
||||
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
|
||||
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
|
||||
autoAssetService: createFallbackAutoAssetService('summary'),
|
||||
});
|
||||
const userId = 'user-phase3-summary';
|
||||
const readySession = await createReadySession(orchestrator, userId);
|
||||
@@ -264,10 +396,70 @@ test('phase3 work summaries prefer compiled foundation draft fields', async () =
|
||||
customWorldAgentSessions: sessionStore,
|
||||
});
|
||||
const draft = items.find((item) => item.sessionId === readySession.sessionId);
|
||||
const compiledProfile = normalizeFoundationDraftProfile(
|
||||
(
|
||||
await orchestrator.getSessionSnapshot(userId, readySession.sessionId)
|
||||
)?.draftProfile,
|
||||
);
|
||||
const totalRoleCount = [
|
||||
...new Set(
|
||||
[
|
||||
...(compiledProfile?.playableNpcs ?? []),
|
||||
...(compiledProfile?.storyNpcs ?? []),
|
||||
].map((entry) => entry.id),
|
||||
),
|
||||
].length;
|
||||
|
||||
assert.ok(draft);
|
||||
assert.ok((draft?.playableNpcCount ?? 0) >= 3);
|
||||
assert.ok((draft?.landmarkCount ?? 0) >= 4);
|
||||
assert.equal(draft?.playableNpcCount ?? 0, totalRoleCount);
|
||||
assert.equal(draft?.landmarkCount ?? 0, 2);
|
||||
assert.match(draft?.summary ?? '', /潮雾|守灯|航道/u);
|
||||
assert.match(draft?.subtitle ?? '', /守灯|冲突|列岛/u);
|
||||
});
|
||||
|
||||
test('phase3 draft foundation still completes when auto asset generation fails', async () => {
|
||||
const runtimeRepository = createRuntimeRepositoryStub();
|
||||
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
|
||||
const autoAssetService = new CustomWorldAgentAutoAssetService(
|
||||
createAutoAssetTestConfig('asset-failure'),
|
||||
async () => {
|
||||
throw new Error('visual service timeout');
|
||||
},
|
||||
async () => {
|
||||
throw new Error('scene service timeout');
|
||||
},
|
||||
);
|
||||
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
|
||||
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
|
||||
autoAssetService,
|
||||
});
|
||||
const userId = 'user-phase3-asset-failure';
|
||||
const readySession = await createReadySession(orchestrator, userId);
|
||||
|
||||
const response = await orchestrator.executeAction(
|
||||
userId,
|
||||
readySession.sessionId,
|
||||
{
|
||||
action: 'draft_foundation',
|
||||
},
|
||||
);
|
||||
const operation = await waitForOperation(
|
||||
orchestrator,
|
||||
userId,
|
||||
readySession.sessionId,
|
||||
response.operation.operationId,
|
||||
);
|
||||
const snapshot = await orchestrator.getSessionSnapshot(userId, readySession.sessionId);
|
||||
|
||||
assert.equal(operation?.status, 'completed');
|
||||
assert.doesNotMatch(operation?.phaseDetail ?? '', /资产补齐待后续处理/u);
|
||||
assert.ok(snapshot?.draftCards.length);
|
||||
assert.ok(
|
||||
snapshot?.messages.every(
|
||||
(message) =>
|
||||
message.role !== 'assistant' || !message.text.includes('资产补齐未完成'),
|
||||
),
|
||||
);
|
||||
assert.equal(snapshot?.assetCoverage.allRoleAssetsReady, true);
|
||||
assert.equal(snapshot?.assetCoverage.allSceneAssetsReady, true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user