This commit is contained in:
2026-04-21 18:27:46 +08:00
parent 04bff9617d
commit 4372ab5be1
358 changed files with 30788 additions and 14737 deletions

View File

@@ -1,94 +1,13 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import type { CustomWorldSessionRecord } from '../../../packages/shared/src/contracts/runtime.js';
import type { RuntimeRepositoryPort } from '../repositories/runtimeRepository.js';
import { normalizeFoundationDraftProfile } from './customWorldAgentDraftCompiler.js';
import { CustomWorldAgentOrchestrator } from './customWorldAgentOrchestrator.js';
import { createInMemoryRpgWorldRepositoryPorts } from './customWorldAgentRepositoryTestHelpers.js';
import { CustomWorldAgentSessionStore } from './customWorldAgentSessionStore.js';
import { createTestCustomWorldAgentSingleTurnLlmClient } from './customWorldAgentTestHelpers.js';
import { listCustomWorldWorkSummaries } from './customWorldWorkSummaryService.js';
function createRuntimeRepositoryStub(): RuntimeRepositoryPort {
const sessionsByUser = new Map<
string,
Map<string, CustomWorldSessionRecord>
>();
const profilesByUser = new Map<string, Record<string, unknown>[]>();
const getSessionBucket = (userId: string) => {
const existing = sessionsByUser.get(userId);
if (existing) {
return existing;
}
const nextBucket = new Map<string, CustomWorldSessionRecord>();
sessionsByUser.set(userId, nextBucket);
return nextBucket;
};
return {
async getSnapshot(_userId) {
return null;
},
async putSnapshot(_userId, _payload) {
throw new Error('not implemented');
},
async deleteSnapshot(_userId) {
return undefined;
},
async getSettings() {
return {
musicVolume: 0.42,
platformTheme: 'light',
};
},
async putSettings(_userId, settings) {
return settings;
},
async listCustomWorldProfiles(userId) {
return [...(profilesByUser.get(userId) ?? [])];
},
async upsertCustomWorldProfile(userId, profileId, profile) {
const current = [...(profilesByUser.get(userId) ?? [])].filter(
(item) => String(item.id ?? '') !== profileId,
);
current.unshift({
...profile,
id: profileId,
});
profilesByUser.set(userId, current);
return current;
},
async deleteCustomWorldProfile(userId, profileId) {
const current = [...(profilesByUser.get(userId) ?? [])].filter(
(item) => String(item.id ?? '') !== profileId,
);
profilesByUser.set(userId, current);
return current;
},
async listProfileSaveArchives() {
return [];
},
async resumeProfileSaveArchive() {
return null;
},
async listCustomWorldSessions(userId) {
return [...getSessionBucket(userId).values()];
},
async getCustomWorldSession(userId, sessionId) {
return getSessionBucket(userId).get(sessionId) ?? null;
},
async upsertCustomWorldSession(userId, sessionId, session) {
getSessionBucket(userId).set(
sessionId,
JSON.parse(JSON.stringify(session)),
);
return JSON.parse(JSON.stringify(session));
},
};
}
async function waitForOperation(
orchestrator: CustomWorldAgentOrchestrator,
userId: string,
@@ -167,8 +86,10 @@ async function createObjectRefiningSession(
}
test('phase4 update_draft_card writes back draft profile and recompiles summaries', async () => {
const runtimeRepository = createRuntimeRepositoryStub();
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
const { rpgAgentSessionRepository } = createInMemoryRpgWorldRepositoryPorts();
const sessionStore = new CustomWorldAgentSessionStore(
rpgAgentSessionRepository,
);
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
});
@@ -228,8 +149,10 @@ test('phase4 update_draft_card writes back draft profile and recompiles summarie
});
test('phase4 sync_result_profile writes result-page snapshot back into session draft chain', async () => {
const runtimeRepository = createRuntimeRepositoryStub();
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
const { rpgAgentSessionRepository } = createInMemoryRpgWorldRepositoryPorts();
const sessionStore = new CustomWorldAgentSessionStore(
rpgAgentSessionRepository,
);
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
});
@@ -290,6 +213,15 @@ test('phase4 sync_result_profile writes result-page snapshot back into session d
profile?.summary,
'结果页已经把世界概述继续往沉船夜暗线收紧。',
);
assert.equal(snapshot?.resultPreview?.source, 'session_preview');
assert.equal(
snapshot?.resultPreview?.preview.name,
'潮雾列岛·结果页精修版',
);
assert.equal(
snapshot?.resultPreview?.preview.playerGoal,
'查清沉船夜与假航灯的真正操盘者。',
);
assert.equal(legacyResultProfile?.name, '潮雾列岛·结果页精修版');
assert.equal(
legacyResultProfile?.playerGoal,
@@ -305,8 +237,10 @@ test('phase4 sync_result_profile writes result-page snapshot back into session d
});
test('phase4 sync_result_profile keeps existing foundation structure while updating summary snapshot', async () => {
const runtimeRepository = createRuntimeRepositoryStub();
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
const { rpgAgentSessionRepository } = createInMemoryRpgWorldRepositoryPorts();
const sessionStore = new CustomWorldAgentSessionStore(
rpgAgentSessionRepository,
);
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
});
@@ -422,8 +356,10 @@ test('phase4 sync_result_profile keeps existing foundation structure while updat
});
test('phase4 sync_result_profile also writes latest role and scene assets back into draft profile', async () => {
const runtimeRepository = createRuntimeRepositoryStub();
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
const { rpgAgentSessionRepository } = createInMemoryRpgWorldRepositoryPorts();
const sessionStore = new CustomWorldAgentSessionStore(
rpgAgentSessionRepository,
);
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
});
@@ -578,8 +514,11 @@ test('phase4 sync_result_profile also writes latest role and scene assets back i
});
test('phase4 generate_characters appends story npcs and updates work summary counts', async () => {
const runtimeRepository = createRuntimeRepositoryStub();
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
const { rpgAgentSessionRepository, rpgWorldProfileRepository } =
createInMemoryRpgWorldRepositoryPorts();
const sessionStore = new CustomWorldAgentSessionStore(
rpgAgentSessionRepository,
);
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
});
@@ -614,7 +553,7 @@ test('phase4 generate_characters appends story npcs and updates work summary cou
),
].length;
const workItems = await listCustomWorldWorkSummaries(userId, {
runtimeRepository,
rpgWorldProfiles: rpgWorldProfileRepository,
customWorldAgentSessions: sessionStore,
});
const draftItem = workItems.find((item) => item.sessionId === session.sessionId);
@@ -634,8 +573,10 @@ test('phase4 generate_characters appends story npcs and updates work summary cou
});
test('phase4 generate_landmarks appends new landmark cards and checkpoints', async () => {
const runtimeRepository = createRuntimeRepositoryStub();
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
const { rpgAgentSessionRepository } = createInMemoryRpgWorldRepositoryPorts();
const sessionStore = new CustomWorldAgentSessionStore(
rpgAgentSessionRepository,
);
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
});
@@ -675,25 +616,33 @@ test('phase4 generate_landmarks appends new landmark cards and checkpoints', asy
});
test('phase4 work summaries exclude library draft entries after phase3 downgrade', async () => {
const runtimeRepository = createRuntimeRepositoryStub();
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
const { rpgAgentSessionRepository, rpgWorldProfileRepository } =
createInMemoryRpgWorldRepositoryPorts();
const sessionStore = new CustomWorldAgentSessionStore(
rpgAgentSessionRepository,
);
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
});
const userId = 'user-phase4-work-summary-phase3';
const session = await createObjectRefiningSession(orchestrator, userId);
await runtimeRepository.upsertCustomWorldProfile(userId, 'library-draft-1', {
id: 'library-draft-1',
name: '旧兼容草稿',
subtitle: '仍保留在作品库',
summary: '不应该继续出现在创作中心 works 聚合里。',
playableNpcs: [],
landmarks: [],
});
await rpgWorldProfileRepository.upsertOwnProfile(
userId,
'library-draft-1',
{
id: 'library-draft-1',
name: '旧兼容草稿',
subtitle: '仍保留在作品库',
summary: '不应该继续出现在创作中心 works 聚合里。',
playableNpcs: [],
landmarks: [],
},
'玩家',
);
const workItems = await listCustomWorldWorkSummaries(userId, {
runtimeRepository,
rpgWorldProfiles: rpgWorldProfileRepository,
customWorldAgentSessions: sessionStore,
});
@@ -703,3 +652,54 @@ test('phase4 work summaries exclude library draft entries after phase3 downgrade
false,
);
});
test('phase4 work summaries hide published agent sessions from draft lane and keep published entry enterable', async () => {
const { rpgAgentSessionRepository, rpgWorldProfileRepository } =
createInMemoryRpgWorldRepositoryPorts();
const sessionStore = new CustomWorldAgentSessionStore(
rpgAgentSessionRepository,
);
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
});
const userId = 'user-phase4-work-summary-published';
const session = await createObjectRefiningSession(orchestrator, userId);
await sessionStore.replaceDerivedState(userId, session.sessionId, {
stage: 'published',
qualityFindings: [],
});
await rpgWorldProfileRepository.upsertOwnProfile(
userId,
`agent-draft-${session.sessionId}`,
{
id: `agent-draft-${session.sessionId}`,
name: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '已发布版本。',
playableNpcs: [],
landmarks: [],
},
'玩家',
);
await rpgWorldProfileRepository.publishOwnProfile(
userId,
`agent-draft-${session.sessionId}`,
'玩家',
);
const workItems = await listCustomWorldWorkSummaries(userId, {
rpgWorldProfiles: rpgWorldProfileRepository,
customWorldAgentSessions: sessionStore,
});
const draftItem = workItems.find((item) => item.sessionId === session.sessionId);
const publishedItem = workItems.find(
(item) => item.profileId === `agent-draft-${session.sessionId}`,
);
assert.equal(draftItem, undefined);
assert.equal(publishedItem?.status, 'published');
assert.equal(publishedItem?.canEnterWorld, true);
assert.equal(publishedItem?.publishReady, true);
assert.equal(publishedItem?.blockerCount, 0);
});